Skip to content

[Backport release-1.6] fix(release): make promote-retag digest verification media-type-agnostic - #3473

Merged
myasnikovdaniil merged 2 commits into
release-1.6from
backport-3435-to-release-1.6
Jul 28, 2026
Merged

[Backport release-1.6] fix(release): make promote-retag digest verification media-type-agnostic#3473
myasnikovdaniil merged 2 commits into
release-1.6from
backport-3435-to-release-1.6

Conversation

@github-actions

Copy link
Copy Markdown

Description

Backport of #3435 to release-1.6.

skopeo inspect --format '{{.Digest}}' fails on OCI artifacts: for
cozystack-packages (config.mediaType
application/vnd.cncf.flux.config.v1+json) it prints nothing and exits
nonzero, so the v1.6.0 finalize aborted its post-copy verification right
after both copies had already succeeded, leaving 32 of 43 repos without
stable tags and skipping the cozy-installer publish. --dry-run never
executes this branch, so rehearsals could not catch it.

Compute the digest as sha256 of the raw manifest instead, which is how
registries define it and works for any media type. The helper gates the
hash on skopeo's exit status via a temp file rather than a pipeline:
a missing tag still yields an empty string (pre-check reads that as
"tag absent, proceed to copy"), never the empty-input hash
sha256:e3b0c442..., and never an abort under set -eu.

Verified against live GHCR: the raw-manifest hash of
cozystack-packages:v1.6.0 matches the digest finalize expected
(bf68208730860fa8...), and agrees byte-for-byte with the old method on
regular multi-arch images. New cozytest cases cover the OCI-artifact
digest, a post-copy mismatch, and the missing-tag path.

Assisted-By: GPT-5 <noreply@openai.com>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
(cherry picked from commit 5d8dd51)
Review notes from #3435, and a correction to my first attempt at them.

manifest_digest gated on skopeo`s exit status alone, so a zero exit with no
bytes hashed the empty string to sha256:e3b0c442... — a digest that looks real
and belongs to no manifest. The first fix simply required non-empty bytes, which
was worse than it looked: the caller turns an empty result into "not published,
safe to copy", so a registry answering 200 with an empty body went from an
accidental refusal (the bogus digest mismatched, so the write-once check
refused) to an actual copy over whatever the tag really held. Verified against
real skopeo 1.23: a 200 with Content-Length 0 does exit 0 with no stdout.

The helper is now explicitly three-state. A digest, or empty output for a tag the
registry PROVED absent, or non-zero for anything indeterminate — which under
set -e aborts the promotion before it writes. Absence has to be reported as
"manifest unknown"; 429, 5xx, auth and network failures all exit non-zero with
no bytes too, and reading those as "unpublished" is the same fail-open one level
down. finalize retags ~42 refs with no retry wrapper, so a single rate-limit was
enough to reach that path. skopeo`s stderr is captured instead of discarded, so
the diagnostics now name the real failure.

Tests: the write-once refusal branch the review found untested (an existing
stable tag at a different digest must be refused before any write), the empty-
body case (refuses, writes nothing, and the empty-input hash never appears in
the diagnostics), and a 429 that must not read as an absent tag. The mock now
emits realistic registry errors, since a bare non-zero exit is no longer proof
of absence. Both new guards are mutation-checked: accepting any non-zero exit as
absence fails the 429 test, dropping the empty-bytes check fails the empty-body
test.

11/11 tests green, hack/image-pin-consistency.bats still 2/2, sh -n clean.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
(cherry picked from commit ae39475)
@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) backport Should change be backported on previous release kind/bug Categorizes issue or PR as related to a bug labels Jul 28, 2026
@myasnikovdaniil
myasnikovdaniil merged commit 96a76cb into release-1.6 Jul 28, 2026
6 of 7 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the backport-3435-to-release-1.6 branch July 28, 2026 12:24
myasnikovdaniil added a commit that referenced this pull request Aug 4, 2026
## Why

`release-1.6` was created at v1.6.0's merge commit on 2026-07-22, and
the finalize fixes landed on `main` after that. Workflow files run from
the ref they fire on, so a promote PR based on `release-1.6` runs
**this** copy of `pull-requests-release.yaml`, not `main`'s. Shipping
v1.6.1 from this line today would repeat two defects v1.6.0 already hit.

## What this carries

Three upstream commits, after which this file is **byte-identical to
`main`'s copy** — so later backports touching it will not conflict.

| Upstream | Change |
| --- | --- |
| `01e1e7188` | `persist-credentials: false` on the checkout |
| `f49d54a68` | publish the release with the merged changelog as its
body |
| `ba67fea7d` | drop `paths-ignore` from the trigger |

### persist-credentials

Without it the checkout persists `GITHUB_TOKEN` as an
`http.extraheader`, which silently wins over the app token injected by
`git remote set-url`. The stable tag then pushes as `GITHUB_TOKEN`,
which creates no workflow run, so `tags.yaml` never fires and its
`generate-changelog` and `update-website-docs` backstops stay silent.
That is exactly what happened to v1.6.0.

### Changelog as release body

Without it the release publishes with the draft's own body, `"Promoted
from vX.Y.Z-rc.N"`.

**These two compound into a permanent defect on a maintenance line.** A
patch's changelog is committed to `release-1.6.1`, merged into
`release-1.6`, and never reaches `main` — and `update-releasenotes.yaml`
only watches `main`, so nothing would ever sync it. The tag-time
backstop that would have ported it is dead because of the credential
bug. v1.6.1 would ship with placeholder release notes permanently, with
no error anywhere.

### paths-ignore

Latent rather than active. A promote PR carrying only
`docs/changelogs/vX.Y.Z.md` would be dropped by the filter, producing no
finalize run, no tag and no error. v1.6.1 will carry tag-string rewrites
so it would not have fired, but the filter has no remaining purpose now
that the promote PR always carries a changelog.

## On the cherry-pick policy

`docs/release.md`'s skip rule says CI-only changes do not belong in a
patch. That rule governs release *contents*; backporting release
*machinery* so the line is releasable at all is a different thing, and
there is precedent already on this branch — #3473, #3474 and #3514 are
all CI/release backports.

## Verification

`actionlint` clean. `zizmor` clean, no findings. YAML parses. No bats
suite references this workflow, and `hack/promote-gate-contract.bats`
does not exist on this line, so there is no stale contract test to trip.
The diff against `origin/main`'s copy of the file is empty.

## Not included

Three known gaps on this line are deliberately out of scope, none of
which needs a code change to ship v1.6.1:

- `e2e-tag.yaml` is absent and `tags.yaml` has no `rc-e2e` job, so no
E2E runs on the rc. Handled by dispatching **E2E Release Tag from
`main`** against the published rc tag, which also supplies the promote
gate's alternate evidence.
- `pull-requests.yaml` has no `labeled` trigger, so adding `full-e2e` to
the promote PR starts nothing. Backporting that needs the label-event
guards too, and rc-time validation makes it unnecessary here.
- finalize is still the pre-#3456 monolith, so a mid-registry failure is
not re-runnable and needs the documented hand recovery.

```release-note
NONE
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) backport Should change be backported on previous release kind/bug Categorizes issue or PR as related to a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant