Commit Graph
6 Commits
Author SHA1 Message Date
Myasnikov Daniil e85482bacc ci(release): complete the candidate-aware promotion pipeline on release-1.6
Promote RC refuses to promote a release whose target base cannot verify
the packages candidate. It checks five markers on the base branch before
its first registry write and fails closed with "Target base
'release-1.6' lacks '<path>'". This branch satisfied one of the five
(hack/lib/image-refs.sh), so the next patch release off release-1.6
could not be dispatched at all.

Add the two halves the base has to carry:

  * hack/verify-promoted-packages.sh and hack/lib/promoted-packages.sh,
    byte-identical to main. The verifier pulls the candidate by its
    committed digest and proves it matches the merge tree apart from the
    artifact's impossible self-reference, that promotion rewrote every
    version-line image tag, and that the container repo@digest set is
    unchanged from the rc artifact.

  * the two jobs that run it. verify-release-candidate in
    pull-requests.yaml checks GitHub's prospective merge tree so a
    packages/ edit pushed to the promote PR shows up as a failed check,
    and a Verify stable packages candidate step in the finalize job of
    pull-requests-release.yaml repeats it against the real merge commit.

Position is the whole point of the finalize half: it runs before the
write-once stable tag, the draft-release publish and the retag, so a bad
candidate is refused while nothing stable-named exists yet. The former
"Set up toolchain (skopeo, yq, helm)" and "Login to registry (GHCR)"
steps moved up with it and are no longer duplicated later in the job;
the toolchain now also installs flux and pins yq by version and sha256,
because an irreversible step must not depend on whatever
releases/latest/download resolves to today.

verify-release-candidate is keyed on the promote PR's author and its
head-branch shape rather than on the `release` label main matches. This
branch's on.pull_request.types has no `labeled` event, and the label is
applied about a second after the PR opens (a separate call in
gh pr create), so the `opened` payload carries no labels and a
label-keyed guard would never fire here.

.release-tooling is gitignored, matching main, so the trusted-base
checkout can never be staged.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-17 14:52:28 +05:00
Myasnikov Daniil fa4ae65d04 test(ci): assert usable refs, quoted keys, and shape-4 over-collection
Round-4 review. Both reviewers independently found the declared-extra
test was passing while the thing it guards was broken, and each found one
more.

That test asserted `grep -q "@sha256:<digest>"`, which any token
containing the digest satisfies. Its fixture was a split-host ref, which
a textual scrape can never reconstruct -- only the contiguous tag value
is recoverable, so ref_repo() reduces it to `latest` and every consumer
drops it at the ownership filter. The fixture was therefore
enumerated-but-uncollected, which is exactly the conflation the test was
added to catch, and gutting the scrape to emit a bare digest with no
repository left the whole suite green. The fixture is now a contiguous
ref in an unparseable template (single-quoted, so the scrape's new
quote-tolerance is exercised) and the assertion pins the exact canonical
ref. The limitation that a split-host ref in an unparseable file is
recoverable by neither route is recorded rather than implied away: the
file-level marker check is what covers that case.

The scrape also captured a leading quote, so any quoted ref in a .tag
file or declared extra yielded a token every consumer silently discards.
Nothing in the tree is affected today, but it meant the double-pass
rescued only unquoted refs.

Markers missed quoted YAML keys: an unenumerated template using
"registry": / "repository": passed the entire suite. They now tolerate
quoting on the key as well as the value, still matching 43 files with no
new allowlist entries.

Shape 4 could be broadened from .global.images[] to every map in the
document and pass everything, because asserting that the expected ref
appears cannot see an unexpected one. That scoping is load-bearing -- the
host is a document-level key, so a global binding staples kubeovn's
registry onto unrelated repositories and manufactures owned refs that
were never built, which promotion would then retag and mirror. The shape-4
fixture now carries a digest-pinned sibling outside global.images and the
test asserts its absence.

Two per-shape fixture gaps closed while there: shape 2 had no sibling
`registry:`, so it losing its rejoin was invisible here and caught only
by one nightly-mirror fixture; and shape 1 had no tagless case, so
narrowing it to require a :tag before the digest went undetected -- a
digest-only pin is an ordinary Helm spelling.

Also corrected the residual-limit comment on the markers. Coverage is
exhaustive, not merely broad: the collector joins registry + "/" +
repository, so a split can only fall on a path-separator boundary and
markers 1-3 cover all four possibilities. A split elsewhere does not need
covering because it does not produce a first-party ref.

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-21 22:05:48 +05:00
Myasnikov Daniil 8ad25b970f test(ci): pin digests exactly and generalize the first-party markers
Round-3 review, both reviewers again converging on the same defects.

The file-level marker check was keyed to the exact ref spellings present
in the tree rather than to the join semantics _collect_yaml_shapes
implements, and three realistic first-party refs evaded it: `registry:
ghcr.io/cozystack/cozystack` + `repository: <name>` (kubeovn's layout
under the key name keycloak-operator and ingress-nginx use, which the old
marker missed because it required a trailing slash a complete-value host
does not have), a host+org split with precedent at fluxcd's values.yaml,
and a single-quoted `repository:` where only double quotes were admitted.
All three carried a version-free latest@sha256: tag -- exactly the
profile the check exists for -- and passed the whole suite. The markers
now follow the collector's join semantics; this matches 43 files and
needs no new allowlist entries. The residual limit (a host split at an
arbitrary point inside cozystack/cozystack) is stated rather than
implied, along with why widening further is not worth it: a bare
cozystack/cozystack marker matches 65 files and would need 23 allowlist
entries for Makefiles and test fixtures.

More seriously, making the completeness oracle share shape logic
introduced a regression the previous independent oracle would have
caught: corrupting the last hex digit of every emitted digest left the
rewrite, retag and mirror suites entirely green, because both sides of
the comparison move together and the per-shape test matched only a
repository substring. The digest is the only part deciding which bytes
get retagged, so the per-shape test now asserts the exact canonical
repo@sha256:digest for every shape.

Declared extras were also narrowed by the refactor: parsing alone means a
ref inside a block scalar yields the enclosing block (which the ownership
filter then discards) and an extra that stops parsing after `make update`
yields nothing at all. Both are silent skips, in the one storage shape
defined as a manifest vendored verbatim from upstream. Extras get the
textual scrape again alongside parsing, with the callers' dedup absorbing
the overlap.

Finally, enumerating a path only proves the path is visited: a declared
extra whose ref the parser cannot reach stayed enumerated but uncollected
while both completeness checks passed. Added a test asserting a declared
extra yields its ref, using an unparseable Helm template with a
single-quoted split-host ref -- the shape that defeated both guards.

Also corrected the claim that the per-shape test compensates for the
shared oracle: it catches a shape being DELETED, not narrowed. A shape
losing its registry rejoin is caught by promote-retag and nightly-mirror,
which assert named packages, and the comment now says so.

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-21 21:38:26 +05:00
Myasnikov Daniil 648eb42cae test(ci): make the image-ref guards see split-host and unparseable files
Both round-2 reviewers independently landed on the same two test-side
holes, and both are the guard being blind in exactly the way the bug it
guards was.

The completeness oracle derived its expected set from a grep for a
contiguous ghcr.io/cozystack/cozystack/...@sha256: string, so it could
not see any split-host shape: cilium, keycloak-operator, kubeovn, the OCI
artifact and seven shape-3 refs, 12 of 43 first-party refs in all. That
includes keycloak-operator, which the test's own rationale names as the
motivating profile. The fixture-discovery grep in the same file had
already been widened for this exact reason; the fix was never carried to
the oracle.

The oracle now applies the same shape knowledge to an independently
discovered file list, which is the axis that actually drifts -- what each
consumer disagreed about historically is which FILES it visits, not how a
ref is spelled inside one. To keep that sharing safe, shape coverage is
now pinned by its own test, so deleting a shape still fails something
even though both sides of the comparison would move together.

Sharing required splitting collect_refs_from_file out of
collect_image_refs. Behaviour is unchanged: same 67 refs collected before
and after, multus and the packages artifact both still present.

A YAML oracle cannot cover everything, though. A reviewer planted a
split-host ref in a Helm template, which yq cannot parse at all, and it
survived every test. A file-level marker check now backstops that: any
file carrying a first-party marker must be enumerated or explicitly
allowlisted. Two files are allowlisted today, each with its reason -- the
talos-csr-signer Dockerfile placeholder, and the documented capi gzip
gap. This also closes the secondary weakness that comparing on repo@digest
masks an unenumerated occurrence when the same image sits at an
enumerated path, which is live for that capi file.

Second hole: the postcondition's key-position branch had no coverage.
Replacing the whole pattern with the naive "${RC}@sha256:" -- the
narrowing rejected because cilium keeps tag: and digest: under separate
keys -- left all 22 tests green, so the branch that makes the split shape
visible was asserted by nothing. Added a cilium-shaped fixture at an
unenumerated path.

Both previously-surviving mutations are now caught. The oracle was also
pre-filtered by registry marker before parsing, cutting it from 32s to 5s.

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-21 21:08:15 +05:00
Myasnikov Daniil 9bd8c17971 fix(ci): shape-filter the promote postcondition and close test gaps
Review of the previous commit found the postcondition itself could abort
a release. It grepped the bare "X.Y.Z-rc.N" substring across the tree, and
three live files carry such strings without being image refs:
kubevirt-csi-driver's go.sum pins github.com/golang/protobuf v1.4.0-rc.2
-- a version cozystack actually cut, so promoting v1.4.0 would have
failed on a Go checksum line -- metallb_test.yaml names v1.5.0-rc.2 in a
comment, and the console pnpm-lock.yaml carries rolldown@1.0.0-rc.15. The
remediation the error message gave would have made things worse: declaring
go.sum as a ref source would sed a checksum file.

Match the version only in image-reference position now: after an
image/repository/tag key with no intervening '#', or immediately before an
@sha256 digest. Narrowing it to "${RC}@sha256:" alone would have been
wrong -- cilium keeps `tag: v1.5.0` and `digest:` under separate keys, so
the version is not always adjacent to the digest. Verified against four
real rc versions that previously aborted.

The suite also only tested the fail-closed direction, which is why that
defect survived, and the collector had no completeness guard at all: a
first-party ref added at an unenumerated path on a `latest@` tag was
mirrored by nothing and passed all eighteen tests. Since the mirror's
failure mode is the worst of the three -- the published nightly points at
the private build registry -- that gap mattered most where coverage was
thinnest.

Added: a fail-open test using the three real rc versions above; a
collector completeness diff that discovers refs by content and fails on
any the enumeration misses; coverage for the OCI-artifact shape, whose
deletion previously left every suite green; and an unreadable-file test,
since a read error was being treated as "no match" and silently skipped.

The component-versioned test was vacuous -- its rc version shared no
substring with the kamaji tag it claimed to protect, so it passed by
construction. It now uses an rc version sharing kamaji's 0.19.0 prefix,
which a loosely-anchored implementation would corrupt.

Round-trip fixture discovery matched a combined 'ghcr.io/cozystack/
cozystack/' prefix, which skips keycloak-operator (host in a sibling
registry: key) and kubeovn (host in global.registry.address). It now
matches 'cozystack/cozystack' and stamps split `tag:` keys too.

Also documented two pre-existing gaps rather than half-fixing them: the
nightly host rewrite is a contiguous-substring replace and cannot reach a
split host, and a ref inside a gzip is invisible to every tool including
the postcondition, which skips binaries. Both are host-dimension defects;
the version dimension this change owns is unaffected, because the version
always lives in a tag key regardless of where the host sits.

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-21 20:42:28 +05:00
Myasnikov Daniil 09323fff13 fix(ci): cover every image-ref storage shape in promote, retag and mirror
Promotion rewrote the rc version substring across the depth-2 package
values.yaml plus packages/apps/kubernetes/images/*.tag alone, on the
premise that the kubernetes app was the only one whose .tag files carry
the cozystack version. Nine other .tag files do, and system/multus seds
its ref straight into a vendored upstream manifest. v1.6.0 was therefore
staged with 33 of 55 first-party refs still reading v1.6.0-rc.4.

promote-retag.sh and nightly-mirror.sh had the same blind spot from the
other direction: both scanned only the depth-2 values.yaml, so those
images were never retagged to the stable version (30 refs selected
before, 42 after) and never mirrored to the public registry for a
nightly. A retag miss merely leaves an image short of a tag, since the
digest still resolves inside one registry; a mirror miss is worse,
because the host rewrite walks the same file list and a ref that is
neither mirrored nor rewritten leaves the published tree pointing at the
private build registry.

The three call sites each carried their own idea of where a ref can
live, which is how they drifted apart. Move that knowledge into
hack/lib/image-refs.sh as the single enumeration -- image_ref_files()
for the files, collect_image_refs() for the refs inside them -- and have
all three source it, so a new storage location is declared once and
reaches every consumer.

The rewrite itself moves out of promote-rc.yaml into
hack/promote-rewrite-tags.sh. Being workflow-inline is why this was only
observable by cutting a release: there was nothing to unit test. It now
also asserts its own postcondition, scanning wider than it rewrites and
failing the promotion when any ref survives in a location the
enumeration does not know about, rather than shipping it quietly.

Component-versioned images (kamaji's v0.19.0-cozystack.N) and
third-party pass-through images do not ride the cozystack version line
and are deliberately left alone; a test pins that so a future fix here
cannot over-reach. capi-providers-cpprovider is a documented known gap:
its ref is also embedded in a gzipped copy that the chart actually
ships, and rewriting only the readable copy would diverge the two.

docs/agents/image-refs.md records the contract -- three tag classes,
three storage shapes, the invariants, and which consumer does what.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-21 20:14:24 +05:00