From abddbef4a92e86232606bdc1ece7de97e68f2136 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 25 Oct 2026 00:53:13 -0300 Subject: [PATCH] sync: the chain report says which release images.json has an image of The installation pages name the newest release with an image, so a release without one is documented and installed by nobody; the chain report showed every link but that one. Co-Authored-By: Claude Opus 5.5 --- tools/sync/chain.go | 62 ++++++++++++++++++++++++++++++++++++++++- tools/sync/sync_test.go | 3 ++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tools/sync/chain.go b/tools/sync/chain.go index 71736fc..49c3c4a 100644 --- a/tools/sync/chain.go +++ b/tools/sync/chain.go @@ -2,8 +2,11 @@ package main import ( "context" + "encoding/json" "fmt" "regexp" + "slices" + "strconv" "strings" "github.com/spin-stack/go-tools/versions" @@ -25,7 +28,8 @@ var ( ) // chain is the release-chain report: spin-machine as spin main pins it against its newest -// release, when each link's workflow last ran and how, and what spinbox.dev runs. Each line says +// release, the newest spin release images.json has an image of against spin's newest, when each +// link's workflow last ran and how, and what spinbox.dev runs. Each line says // what it could not read rather than stopping the report: it is advice, and the rest of it holds. func chain(ctx context.Context, gh *github.Client) string { var b strings.Builder @@ -48,6 +52,17 @@ func chain(ctx context.Context, gh *github.Client) string { row("spin-machine", fmt.Sprintf("spin main pins %s; the newest release is **%s**", pinned, latest.Tag)) } + switch imaged, newest, err := images(ctx, gh); { + case err != nil: + row("images.json", "could not read: "+err.Error()) + case imaged == newest: + row("images.json", fmt.Sprintf("has an image of spin's newest release, %s", newest)) + default: + // The installation pages name the newest release with an image: until this one has its + // own, the site documents a release nobody can install. + row("images.json", fmt.Sprintf("the newest release with an image is %s; spin's newest is **%s**, which spin-stack/ami's `release.yml` builds one of", imaged, newest)) + } + for _, r := range runs { var list struct { Runs []struct { @@ -96,6 +111,51 @@ func machinePin(ctx context.Context, gh *github.Client) (string, error) { return e.Version, err } +var release = regexp.MustCompile(`^v(\d{8})\.(\d+)$`) + +// images is the newest spin release spin-terraform-aws's images.json has an image of, and spin's +// newest release. +func images(ctx context.Context, gh *github.Client) (string, string, error) { + raw, err := gh.File(ctx, "spin-stack/spin-terraform-aws", "images.json", "main") + if err != nil { + return "", "", err + } + var byRelease map[string]json.RawMessage + if err := json.Unmarshal(raw, &byRelease); err != nil { + return "", "", fmt.Errorf("images.json: %w", err) + } + var have []string + for r := range byRelease { + if release.MatchString(r) { + have = append(have, r) + } + } + if len(have) == 0 { + return "", "", fmt.Errorf("images.json names no release") + } + var latest struct { + Tag string `json:"tag_name"` + } + if err := gh.Get(ctx, "/repos/spin-stack/spin/releases/latest", &latest); err != nil { + return "", "", err + } + return slices.MaxFunc(have, compareReleases), latest.Tag, nil +} + +// compareReleases orders two release names by date and then by number: v20261019.10 is after +// v20261019.9, which the strings' order would not say. +func compareReleases(a, b string) int { + ma, mb := release.FindStringSubmatch(a), release.FindStringSubmatch(b) + for i := 1; i <= 2; i++ { + x, _ := strconv.Atoi(ma[i]) + y, _ := strconv.Atoi(mb[i]) + if x != y { + return x - y + } + } + return 0 +} + // deployment is the release spinbox.dev runs and the module ref it takes the installation from. func deployment(ctx context.Context, gh *github.Client) (string, string, error) { tfvars, err := gh.File(ctx, "spin-stack/spinbox.dev-deploy", "terraform.tfvars", "main") diff --git a/tools/sync/sync_test.go b/tools/sync/sync_test.go index e928fcd..cfa94e9 100644 --- a/tools/sync/sync_test.go +++ b/tools/sync/sync_test.go @@ -122,6 +122,8 @@ func TestChainReportsEachLinkAndWhatItCouldNotRead(t *testing.T) { track: github-release https://github.com/spin-stack/spin-machine `, "/repos/spin-stack/spin-machine/releases/latest": `{"tag_name": "v20261023.01"}`, + "/repos/spin-stack/spin-terraform-aws/contents/images.json?ref=main": `{"v20261019.9": {}, "v20261019.10": {}, "v20261018.01": {}}`, + "/repos/spin-stack/spin/releases/latest": `{"tag_name": "v20261025.01"}`, "/repos/spin-stack/spin/actions/workflows/machine.yml/runs?per_page=1": `{"workflow_runs": [{"created_at": "2026-10-14T09:00:00Z", "status": "completed", "conclusion": "success", "html_url": "https://github.com/spin-stack/spin/actions/runs/1"}]}`, "/repos/spin-stack/spin-terraform-aws/actions/workflows/propose.yml/runs?per_page=1": `{"workflow_runs": []}`, "/repos/spin-stack/spin-terraform-aws/actions/workflows/fresh-install.yml/runs?per_page=1": "404", @@ -131,6 +133,7 @@ func TestChainReportsEachLinkAndWhatItCouldNotRead(t *testing.T) { got := chain(context.Background(), gh) for _, want := range []string{ "| spin-machine | spin main pins v20261014.01; the newest release is **v20261023.01** |", + "| images.json | the newest release with an image is v20261019.10; spin's newest is **v20261025.01**", "| `machine.yml` a spin-machine release proposed to spin | last ran 2026-10-14, [success](https://github.com/spin-stack/spin/actions/runs/1) |", "| `propose.yml` the module proposed to the deployments | never ran |", "| `fresh-install.yml` an installation from nothing, weekly | could not read: GET /repos/spin-stack/spin-terraform-aws/actions/workflows/fresh-install.yml/runs?per_page=1: 404 Not Found",