Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion tools/sync/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package main

import (
"context"
"encoding/json"
"fmt"
"regexp"
"slices"
"strconv"
"strings"

"github.com/spin-stack/go-tools/versions"
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions tools/sync/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading