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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-31 18:12:53 +00:00
1 parent c830c9b8ba
commit 3bfe5667fb
8 files changed
+45 -8

No files matched your search

+11
View File
@@ -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: `<major>.<minor>.<patch>`, or `<major>.<minor>` 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_<FAMILY>_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 <id> --run <id>` 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.
+1 -1
View File
@@ -1 +1 @@
1.91.377.0
1.91.378.0
+1 -1
View File
@@ -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,
+5 -2
View File
@@ -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 (`<major>.<minor>.<patch>`, or
`<major>.<minor>` for PostgreSQL); a floating major or minor tag, `latest` or
another alias fails matrix generation with `FLOATING_<FAMILY>_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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+5 -2
View File
@@ -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<CsoRuntimeBuildStack, string[]> = {
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<CsoRuntimeBuildPlatform, string> {
function assertPinnedFamily(source: unknown, indexImage: unknown, images: unknown, label: string, releaseTag = RELEASE_TAG): Record<CsoRuntimeBuildPlatform, string> {
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'
+20
View File
@@ -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');