fix(ci): publish latest from the root Docker manifest (#2252)

## Description

A successful root Docker image can miss `:latest` when any optional
variant manifest fails. The release workflow currently gates the
standalone `promote-latest` job on the aggregate `docker-manifest`
matrix, so one sibling failure skips promotion even when the signed root
amd64+arm64 manifest exists.

This moves `:latest` promotion into the successful root manifest cell.
Optional variant failures remain visible and continue to fail their
jobs, but they no longer suppress the image used by `headroom install`,
which defaults to `ghcr.io/headroomlabs-ai/headroom:latest`.

Refs #1583

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Publish `:latest` from the root `docker-manifest` matrix cell after
its versioned multi-architecture manifest is created and signed.
- Remove the aggregate `promote-latest` dependency that allowed
unrelated variant failures to suppress publication.
- Keep all existing root, slim, code, and nonroot variants.
- Preserve native linux/amd64 and linux/arm64 manifest assembly.
- Add a focused workflow-contract regression test.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_release_workflows.py -q
-k "docker or latest"`)
- [x] Linting passes (`uv run ruff check
tests/test_release_workflows.py`)
- [x] Formatting passes (`uv run ruff format --check
tests/test_release_workflows.py`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
Focused checks pass: `5 passed, 34 deselected` for `uv run pytest tests/test_release_workflows.py -q -k "docker or latest"`; the full test file has one unrelated Windows `FileNotFoundError` in `test_no_native_tls_in_wheel_build_tree` because its external command is unavailable. `uv run ruff check tests/test_release_workflows.py` and `uv run ruff format tests/test_release_workflows.py --check` pass. The repository-wide format check reports eight pre-existing files outside this target. Proof report: `D:\Repos\.claude\pr-sweep\headroom-PR-TARGET-1583-PROOF.md`.
```

## Real Behavior Proof

- Environment: Windows, Python managed by `uv`, repository
workflow-contract tests; production publication owned by GitHub Actions
and GHCR.
- Exact command / steps: run the focused release-workflow tests; after
merge, inspect the next Docker release run and execute `docker buildx
imagetools inspect ghcr.io/headroomlabs-ai/headroom:latest` without
registry login.
- Observed result: local workflow-contract proof passes for root-owned
promotion and both native architecture inputs; live GHCR publication
remains unverified until the next release.
- Not tested: production GHCR publication before merge.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [x] Workflow comments explain the non-obvious root-only promotion
boundary
- [x] Documentation outside the changelog is unchanged because the CLI
image reference is already correct
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing focused tests pass locally with my changes

## Screenshots (if applicable)

Not applicable.

## Additional Notes

Release run
https://github.com/headroomlabs-ai/headroom/actions/runs/28404020512
demonstrated the cascade: the root manifest succeeded, a nonroot
manifest failed during Buildx setup, and `promote-latest` was skipped.

PR CI can prove the workflow dependency and architecture-preservation
contracts. GHCR availability and anonymous package visibility require
the next production release plus an unauthenticated registry inspection.
This commit is contained in:
Rod Boev
2026-08-12 00:40:02 -04:00
committed by GitHub
parent 3bb02f8f75
commit 5568d738af
2 changed files with 59 additions and 42 deletions
+8 -42
View File
@@ -220,6 +220,7 @@ jobs:
# tags, and that manifest is what users pull by `:tag`.
docker-manifest:
needs: docker-build
if: ${{ always() }}
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
@@ -313,6 +314,11 @@ jobs:
echo "ERROR: no digests downloaded for variant '${{ matrix.variant.name || 'root' }}'" >&2
exit 1
fi
digest_count="$(find "${DIGEST_DIR}" -maxdepth 1 -type f | wc -l)"
if [ "${digest_count}" -ne 2 ]; then
echo "ERROR: expected both architecture digests for variant '${{ matrix.variant.name || 'root' }}', found ${digest_count}" >&2
exit 1
fi
digest_refs=()
for f in "${DIGEST_DIR}"/*; do
digest="$(basename "$f")"
@@ -382,53 +388,13 @@ jobs:
sleep "$sleep_for"
done
promote-latest:
# Re-push the :latest tag pointing at the root variant *after* every
# variant manifest job has finished, so GHCR's package version
# listing (sorted by created_at) shows the root image with :latest
# at the top instead of whichever variant happened to finish last.
needs: docker-manifest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Normalize image name
id: image-name
run: |
image_name="$(printf '%s' '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
printf 'image_name=%s\n' "$image_name" >> "$GITHUB_OUTPUT"
- name: Determine image version
id: version
env:
MANUAL_VERSION: ${{ inputs.version || github.event.inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
version="${MANUAL_VERSION#v}"
if [ -z "$version" ] && [ -n "$RELEASE_TAG" ]; then
version="${RELEASE_TAG#v}"
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag root image as :latest
if: steps.version.outputs.version != ''
if: steps.manifest.outputs.index_digest != '' && matrix.variant.name == '' && steps.version.outputs.version != ''
env:
IMAGE: ${{ env.REGISTRY }}/${{ steps.image-name.outputs.image_name }}
VERSION: ${{ steps.version.outputs.version }}
run: |
# Add a unique annotation so the resulting image index manifest gets
# a new digest, which makes GHCR record a fresh package version with
# current timestamp (otherwise the existing root manifest is reused
# and stays where it was in the version listing).
# Add a unique annotation so GHCR records a fresh root package version.
promoted_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker buildx imagetools create \
--annotation "index:io.headroom.promoted-at=${promoted_at}" \
+51
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from pathlib import Path
import pytest
import yaml
ROOT = Path(__file__).resolve().parent.parent
@@ -17,6 +18,56 @@ def test_docker_workflow_normalizes_repository_name_for_signing() -> None:
assert "steps.image-name.outputs.image_name" in content
def test_docker_latest_promotion_is_owned_by_root_manifest_cell() -> None:
workflow = yaml.safe_load((ROOT / ".github" / "workflows" / "docker.yml").read_text())
jobs = workflow["jobs"]
build = jobs["docker-build"]
manifest = jobs["docker-manifest"]
variants = manifest["strategy"]["matrix"]["variant"]
build_variants = build["strategy"]["matrix"]["variant"]
architectures = build["strategy"]["matrix"]["arch"]
root = next(entry for entry in variants if entry["name"] == "")
nonroot = next(entry for entry in variants if entry["name"] == "nonroot")
promotion = next(
step for step in manifest["steps"] if step["name"] == "Re-tag root image as :latest"
)
command = promotion["run"]
assert len(variants) == 8
assert [entry["name"] for entry in build_variants] == [entry["name"] for entry in variants]
assert len(architectures) == 2
assert {entry["platform"] for entry in architectures} == {"linux/amd64", "linux/arm64"}
assert root["name"] == ""
assert nonroot["name"] == "nonroot"
assert "matrix.variant.name == ''" in promotion["if"]
assert "steps.manifest.outputs.index_digest != ''" in promotion["if"]
assert "steps.version.outputs.version != ''" in promotion["if"]
assert (
promotion["if"]
== "steps.manifest.outputs.index_digest != '' && matrix.variant.name == '' && steps.version.outputs.version != ''"
)
assert '"${IMAGE}:latest"' in command
assert '"${IMAGE}:${VERSION}"' in command
assert "promote-latest" not in jobs
assert manifest["needs"] == "docker-build"
assert manifest["if"] == "${{ always() }}"
step_names = [step["name"] for step in manifest["steps"]]
assert step_names.index("Sign multi-arch index manifest with cosign") < step_names.index(
"Re-tag root image as :latest"
)
manifest_script = next(
step["run"] for step in manifest["steps"] if step["name"] == "Create multi-arch manifest"
)
assert 'digest_count="$(find "${DIGEST_DIR}" -maxdepth 1 -type f | wc -l)"' in manifest_script
assert '"${digest_count}" -ne 2' in manifest_script
assert manifest_script.index('"${digest_count}" -ne 2') < manifest_script.index(
"docker buildx imagetools create"
)
guard_start = manifest_script.index('"${digest_count}" -ne 2')
create_start = manifest_script.index("docker buildx imagetools create")
assert guard_start < manifest_script.index("exit 1", guard_start) < create_start
def test_release_workflow_publishes_both_node_packages_to_github_packages() -> None:
content = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")