From 3bfe5667fb02a45378fa930d5f498a48d9764155 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 31 Oct 2026 11:12:53 -0700 Subject: [PATCH] v1.91.378.0 fix(cso): pin scout-sbom-indexer source to 1.24.0, refuse floating source tags (GST-495) (#3699) * fix(cso): pin the SBOM generator source to release tag 1.24.0 and refuse floating source tags (GST-495) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (exact release tags; two-part only for postgresql) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(cso): source tags must name an exact release Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(cso): the exact-release rule covers every pinned family Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- docs/cso-release-qualification.md | 7 +++++-- lib/cso/images/build-inputs.json | 2 +- package.json | 2 +- scripts/cso-image-matrix.ts | 7 +++++-- test/cso-distribution.test.ts | 20 ++++++++++++++++++++ 8 files changed, 45 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ede05c8..e95086bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.91.378.0] - 2026-10-31 + +**The CSO runtime image workflow passes again after Docker moved its `scout-sbom-indexer:1` tag.** All 10 `validate-native` jobs in Stage CSO Runtime Images were failing with `SOURCE_TAG_MOVED: docker.io/docker/scout-sbom-indexer:1` on every PR that touched CSO paths. Docker pushed release 1.29.0 to the floating `:1` tag on 2026-10-29. The reviewed index `sha256:4b67f29e…` is Docker's 1.24.0 release, and the `1.24.0` tag still points at it. So the SBOM generator source now names `:1.24.0`. The pinned digests and the build-inputs revision are byte-identical, no requalification trigger changes, and the 10 qualified runtimes stay current. + +### Changed +- **`lib/cso/images/build-inputs.json`**: `sbomGenerator.source` is `docker.io/docker/scout-sbom-indexer:1.24.0` instead of `:1`. +- **`scripts/cso-image-matrix.ts`**: every pinned family (SBOM generator, base images, uv) must name an exact release in its source tag: `..`, or `.` for the PostgreSQL base, whose releases have two parts. A floating major or minor tag (`:1`, `:1.24`, `python:3.13-slim`), `latest` or another alias fails with `FLOATING__SOURCE` before any build. An exact tag can still be re-pushed upstream (Docker's official images rebuild them when their Debian base changes), and `SOURCE_TAG_MOVED` still reports that. Moving the generator to 1.29.0 is a separate change that needs a new build-inputs revision and a requalified catalog. + +### Tests +- `test/cso-distribution.test.ts`: `:1`, `:1.24`, `:1.24-slim`, `:latest` and `:stable` are refused for the SBOM generator and for a base image; PostgreSQL accepts `17.2` but not `17`, Python refuses `3.13-slim-bookworm`; and the committed inputs build all 10 rows with the `1.24.0` source. + ## [1.91.377.0] - 2026-10-31 **A passed GitHub Actions run can now clear the remote test gate for the same tree, so a repository stops running its full suite twice per slot.** gbrain paid a 30–45 minute Ubicloud gate and then an 18–20 minute full-ci dispatch on the same tree in every serial slot (GBRA-108: #7872's slot took 5.6 h). `gstack-pregate clear --remote pregate.json/remote_gate --run --run ` accepts one verified run per profile the base branch declares. gstack checks each run against the runner's own record, never against a pasted claim. It reads the GitHub API's run and jobs, the tree git computes for the run's head, and the `tree.json` the run uploaded. Nothing is skipped: the tests still run once, in CI. A repository that declares no profiles is unchanged. diff --git a/VERSION b/VERSION index 72f0d75b2..54d95d95a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.377.0 +1.91.378.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 56cb480ad..6c52ca354 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.377.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.378.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/docs/cso-release-qualification.md b/docs/cso-release-qualification.md index ee6d60a00..e8efdbc84 100644 --- a/docs/cso-release-qualification.md +++ b/docs/cso-release-qualification.md @@ -145,8 +145,11 @@ archive bytes, not a verified response URL. Image publication starts with the committed [`build-inputs.json`](../lib/cso/images/build-inputs.json). Its reviewed revision pins distinct native manifests for every stack and the SBOM generator. Each -profile records the inspected source tag and immutable index for provenance, -then `baseImages` keyed by `linux/amd64` and `linux/arm64`; Python additionally +profile records the inspected source tag and immutable index for provenance. +Every source tag (each base, Python's uv and the SBOM generator) must name an exact release (`..`, or +`.` for PostgreSQL); a floating major or minor tag, `latest` or +another alias fails matrix generation with `FLOATING__SOURCE`. Each profile then records +`baseImages` keyed by `linux/amd64` and `linux/arm64`; Python additionally records native uv manifests. CI fails if a source tag no longer resolves to the reviewed index, a platform manifest is absent from that index, its image configuration names another architecture, or an executable reports a different diff --git a/lib/cso/images/build-inputs.json b/lib/cso/images/build-inputs.json index 06221522c..3689a2b25 100644 --- a/lib/cso/images/build-inputs.json +++ b/lib/cso/images/build-inputs.json @@ -6,7 +6,7 @@ "reviewedAt": "2026-09-10T00:00:00.000Z", "reviewMethod": "Native OCI manifest inspection plus upstream runtime version metadata; CI rechecks architecture and executable versions before every build.", "sbomGenerator": { - "source": "docker.io/docker/scout-sbom-indexer:1", + "source": "docker.io/docker/scout-sbom-indexer:1.24.0", "indexImage": "docker.io/docker/scout-sbom-indexer@sha256:4b67f29eb0d1244ab0f62de867ac5dafd7262fcd7ebbdefa6ec8aacd6b15252d", "images": { "linux/amd64": "docker.io/docker/scout-sbom-indexer@sha256:dc9450ee50d985e1f5cc60069c91b5f45b4a8a7e9dbcdd33aaab259032a9cab9", diff --git a/package.json b/package.json index eb127dd21..a792b2854 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.377", + "version": "1.91.378", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/scripts/cso-image-matrix.ts b/scripts/cso-image-matrix.ts index f07d9d9b6..e6c2226cf 100644 --- a/scripts/cso-image-matrix.ts +++ b/scripts/cso-image-matrix.ts @@ -10,6 +10,8 @@ export type CsoRuntimeBuildStack = typeof CSO_RUNTIME_STACKS[number]; const IMAGE = /^[a-z0-9.-]+(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/; const TAGGED_IMAGE = /^[a-z0-9.-]+(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; +const RELEASE_TAG = /:v?\d+\.\d+\.\d+(?:[._-][A-Za-z0-9._-]*)?$/; +const TWO_PART_RELEASE_TAG = /:v?\d+\.\d+(?:\.\d+)?(?:[._-][A-Za-z0-9._-]*)?$/; const REVISION = /^[a-z0-9][a-z0-9._-]{0,100}$/; const REQUIRED_VERSIONS: Record = { node: ['node', 'npm', 'cso-preparation'], @@ -56,8 +58,9 @@ function repositoryOfTag(image: string): string { return image.slice(0, colon); } -function assertPinnedFamily(source: unknown, indexImage: unknown, images: unknown, label: string): Record { +function assertPinnedFamily(source: unknown, indexImage: unknown, images: unknown, label: string, releaseTag = RELEASE_TAG): Record { if (typeof source !== 'string' || !TAGGED_IMAGE.test(source)) throw new Error(`INVALID_${label}_SOURCE`); + if (!releaseTag.test(source)) throw new Error(`FLOATING_${label}_SOURCE: ${source} must name an exact release version, not a moving major, minor or alias tag`); if (typeof indexImage !== 'string' || !IMAGE.test(indexImage)) throw new Error(`UNPINNED_${label}_INDEX`); if (!record(images)) throw new Error(`MISSING_${label}_IMAGES`); const repository = repositoryOfTag(source); @@ -101,7 +104,7 @@ export function imageBuildMatrix(input: unknown): { include: ImageBuildRow[] } { if (typeof stack !== 'string' || !CSO_RUNTIME_STACKS.includes(stack as CsoRuntimeBuildStack) || seenStacks.has(stack)) throw new Error('INVALID_STACK'); seenStacks.add(stack); const typedStack = stack as CsoRuntimeBuildStack; - const bases = assertPinnedFamily(source, indexImage, baseImages, 'BASE'); + const bases = assertPinnedFamily(source, indexImage, baseImages, 'BASE', typedStack === 'postgresql' ? TWO_PART_RELEASE_TAG : RELEASE_TAG); if (!record(versions)) throw new Error('MISSING_TOOL_VERSIONS'); const exactVersion = (value: unknown) => typeof value === 'string' && (typedStack === 'postgresql' diff --git a/test/cso-distribution.test.ts b/test/cso-distribution.test.ts index dd0417e6b..6a0dab946 100644 --- a/test/cso-distribution.test.ts +++ b/test/cso-distribution.test.ts @@ -368,6 +368,26 @@ describe('CSO runtime staging gates', () => { expect(() => imageBuildMatrix(versions)).toThrow('UNPINNED_TOOL_VERSION'); }); + test.each(['docker.io/tool/sbom:1', 'docker.io/tool/sbom:1.24', 'docker.io/tool/sbom:1.24-slim', 'docker.io/tool/sbom:latest', 'docker.io/tool/sbom:stable'])('floating source tag %s cannot pin a reviewed family', source => { + const sbom = reviewedInputs(); sbom.sbomGenerator.source = source; + expect(() => imageBuildMatrix(sbom)).toThrow('FLOATING_SBOM_GENERATOR_SOURCE'); + const base = reviewedInputs(); base.profiles[0].source = source.replace('docker.io/tool/sbom', 'docker.io/library/node'); + expect(() => imageBuildMatrix(base)).toThrow('FLOATING_BASE_SOURCE'); + }); + + test('PostgreSQL alone may name a two-part release, which is exact for it', () => { + const postgres = reviewedInputs(); postgres.profiles[4].source = 'docker.io/library/postgres:17-bookworm'; + expect(() => imageBuildMatrix(postgres)).toThrow('FLOATING_BASE_SOURCE'); + const python = reviewedInputs(); python.profiles[2].source = 'docker.io/library/python:3.13-slim-bookworm'; + expect(() => imageBuildMatrix(python)).toThrow('FLOATING_BASE_SOURCE'); + expect(imageBuildMatrix(reviewedInputs()).include.find(row => row.stack === 'postgresql')?.baseSource).toBe('docker.io/library/postgres:17.2'); + }); + + test('the committed build inputs name release tags for every source', () => { + expect(buildInputs.sbomGenerator.source).toBe('docker.io/docker/scout-sbom-indexer:1.24.0'); + expect(committedImageBuildMatrix().include).toHaveLength(10); + }); + test('duplicate stacks and missing Python uv images fail precisely', () => { const duplicate = reviewedInputs(); duplicate.profiles[1].stack = 'node'; expect(() => imageBuildMatrix(duplicate)).toThrow('INVALID_STACK');