Release#
When to Use#
- Editing
.github/workflows/promote-testing-to-main.ymlorexecute-release.yml - Debugging why a promotion PR did not auto-merge
- Performing an emergency production rollback
- Verifying published
:stableimages after a release
When NOT to Use#
- Package or image-content changes →
docs/skills/build.md - Non-promotion CI failures →
docs/skills/ci-cd.md - Hardware-specific image issues →
docs/skills/hardware.md
Branch model (factory standard)#
testing → main → :stable
testing— all PRs target this branch. Builds push:testingOCI tag directly on every push.main— production source. Advances only via squash promotion fromtesting. Triggeringexecute-release.yml.- No
ltsbranch in the promotion flow. Theltsgit branch is archived.
Never push directly to main. All changes via PR to testing, then auto-promoted.
Flow is one-way: testing → main. Never merge main → testing manually.
Production release flow#
promote-testing-to-main.ymlfires on push totesting, daily at 04:00 UTC, and on manual dispatch.
It callsreusable-promote-squash.yml@v1withsource_branch: testing,target_branch: main,use_merge_queue: true.
When trees differ it rebuilds the squash branch and upserts theauto/promote-testing-to-mainPR.- The PR enters the merge queue (ruleset 17070416 on
mainrequires squash + merge queue).
Required check:Lint & syntax. No approvals needed. - On merge,
execute-release.ymlfires onpush: main, detects the commit message
"^chore: promote testing to main", skopeo-copies:testing → :stablefor all three variants,
and creates a GitHub release with changelog viareusable-release.yml@v1.
# Check promotion PR status
gh pr list --repo projectbluefin/bluefin-lts --head auto/promote-testing-to-main
# Emergency bypass (CODEOWNERS or merge queue blocking — requires admin)
gh pr merge <pr-number> --repo projectbluefin/bluefin-lts --squash --admin
Branch protection#
main requires a PR with merge queue entry. Gate check: Lint & syntax. 0 approvals required.
.github/workflows/ is CODEOWNERS-protected — PRs touching workflow files require --admin bypass.
Daily cadence#
- Fully automated: daily 04:00 UTC — cron fires, promote workflow updates the PR, merge queue processes it, execute-release publishes
:stable - No human approval required —
Lint & syntaxis the only gate workflow_dispatchis the supported manual release path
Image verification — always check digests#
Do NOT trust "the fix is in main" as evidence the fix is published. Verify:
GHCR_TOKEN=$(gh auth token)
for IMAGE in bluefin-lts bluefin-lts-nvidia; do
skopeo inspect --no-tags --creds "castrojo:${GHCR_TOKEN}" docker://ghcr.io/projectbluefin/${IMAGE}:stable \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('${IMAGE}:stable', d['Digest'][:22], d['Labels'].get('org.opencontainers.image.created','?')[:10])"
done
A fix is published when:
- The
:stabledigest differs from the last known digest - The
org.opencontainers.image.createddate is after the fix merged - Both variants (bluefin-lts and bluefin-lts-nvidia) are updated
Build cascade — rapid commits cancel in-progress builds#
Each push to testing triggers new builds which cancel in-progress builds via concurrency groups. Builds take 60-90 min. Stop committing to testing while builds are in progress.
SHA=<commit-sha>
gh run list --repo projectbluefin/bluefin-lts \
--json workflowName,status,conclusion,headSha \
--jq "[.[] | select(.headSha | startswith(\"$SHA\")) | select(.workflowName | contains(\"Build\"))]"
Registry queries#
gh auth token | skopeo login ghcr.io -u castrojo --password-stdin
skopeo list-tags docker://ghcr.io/projectbluefin/bluefin-lts
skopeo list-tags docker://ghcr.io/projectbluefin/bluefin-lts-nvidia
Images publish to:
ghcr.io/projectbluefin/bluefin-ltsghcr.io/projectbluefin/bluefin-lts-nvidia
Emergency rollback#
GHCR_TOKEN=$(gh auth token)
skopeo copy \
--src-no-creds \
--dest-creds "castrojo:${GHCR_TOKEN}" \
docker://ghcr.io/projectbluefin/IMAGE:stable-YYYYMMDD \
docker://ghcr.io/projectbluefin/IMAGE:stable
Rollback both variants, then verify digest/created time.
Emergency promotion for production-bricking bugs#
- Push fix to
testing— builds trigger automatically on push totesting. - Wait for both builds to complete (~45-90 min). Never promote before builds finish.
- Verify the new
:testingimage has a fresh initramfs (see Verifying images below). - Skopeo-copy
:testing→:stableby digest:
GHCR_TOKEN=$(gh auth token)
for IMAGE in bluefin-lts bluefin-lts-nvidia; do
DIGEST=$(skopeo inspect --no-tags --creds "castrojo:${GHCR_TOKEN}" docker://ghcr.io/projectbluefin/${IMAGE}:testing \
| python3 -c "import json,sys; print(json.load(sys.stdin)['Digest'])")
skopeo copy \
--src-creds "castrojo:${GHCR_TOKEN}" \
--dest-creds "castrojo:${GHCR_TOKEN}" \
docker://ghcr.io/projectbluefin/${IMAGE}@${DIGEST} \
docker://ghcr.io/projectbluefin/${IMAGE}:stable
done
Always copy by digest, not tag — prevents races with concurrent pushes.
Verifying images#
:testing is published directly by the build#
Build workflows push :testing on every push to the testing branch. No E2E gate.
PR builds validate that the image builds but do not push to GHCR.
/boot/ is intentionally empty in the OCI image#
bootc stores the kernel and initramfs under /usr/lib/modules/<kver>/, not /boot/. An empty /boot/ in the container layer is expected and correct.
podman run --rm ghcr.io/projectbluefin/bluefin-lts:stable bash -c '
sha256sum /usr/lib/modules/*/initramfs.img
ls -la /usr/lib/modules/*/vmlinuz
grep BUILD_ID /etc/os-release
'
Initramfs must differ from the previous broken build#
After a dracut-related fix, verify the initramfs SHA changed:
OLD=$(podman run --rm ghcr.io/projectbluefin/bluefin-lts:stable bash -c 'sha256sum /usr/lib/modules/*/initramfs.img' 2>/dev/null | awk '{print $1}')
podman pull ghcr.io/projectbluefin/bluefin-lts:stable
NEW=$(podman run --rm ghcr.io/projectbluefin/bluefin-lts:stable bash -c 'sha256sum /usr/lib/modules/*/initramfs.img' 2>/dev/null | awk '{print $1}')
[ "$OLD" != "$NEW" ] && echo "initramfs changed" || echo "same initramfs — promotion may be a no-op"
ISO status#
LTS ISO is disabled. Do not re-enable. Anaconda is broken on CentOS Stream base.
Red Flags#
use_merge_queue: false— main requires a merge queue (ruleset 17070416); always usetruesource_branch: mainortarget_branch: lts— model changed; usetesting→main- adding
run_e2e: true— no post-merge-e2e gate; builds publish:testingdirectly - describing the schedule as weekly — cadence is daily at 04:00 UTC (
0 4 * * *) - Claiming completion without live verification: Never claim a build-fixing task is "fully complete" without noting that the fix is still pending live verification by the active CI pipeline (which takes 45–90 mins). Always clearly differentiate between local code-level/syntax validation and live OCI container build execution.
Verification#
-
promote-testing-to-main.ymlschedules daily at0 4 * * * -
use_merge_queue: true -
source_branch: testingandtarget_branch: main -
execute-release.ymlfires onpush: main, detects"^chore: promote testing to main", publishes:stable - Build workflows push
:testingon push totestingbranch