HOMELAB-1681: ci(cockpit): workflow_dispatch build path for pre-live image tags #580
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!580
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1681b-image-dispatch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
The LD3.1b deploy slice (ratified:
.rpiv/artifacts/designs/ld3/ratification-package-3.1b.md) pins the cockpit image by digest. The cockpit image workflow's only push trigger islive, which epic/LD3 has not reached — so the digest cannot exist yet. This PR adds the dispatch path that publishes an epic/LD3 build, and it is a prerequisite of the prod deploy PR.Why not just merge the epic first
The deploy slice is a
prod → livePR (prod targetslivedirectly), so it can land before the epic merges. What it cannot do is name an image that was never built.The change — additive by construction
workflow_dispatch{}refinput,default: ""git fetch --depth=1 origin <ref>+checkout --detach FETCH_HEAD, no-op when the input is emptypush/pull_requestnever set the input, and a dispatch with an empty input builds exactly whatactions/checkoutalready holdsSHORT_SHA${{ github.sha }} | cut -c1-7git rev-parse HEAD | cut -c1-7github.shaon push/PR)latestpromotionif GITHUB_REF = refs/heads/livecase "${DISPATCH_REF:-$GITHUB_REF}"acceptinglive/refs/heads/livepushtolivestill promotes and nothing else doesByte-identical and untouched: both existing trigger blocks (
branches/paths), theif: github.event_name != 'pull_request'push gate, the scopedHARBOR_PUSH_USERNAME/HARBOR_PUSH_PASSWORDrobot (no admin credential, no new secret, no new privilege), the SHA-tag-only push, the size gate, all three smoke tests,IMAGE/DOCKERFILE/CONTEXT.Why
SHORT_SHAmoved togit rev-parse HEAD. With arefinput,github.shais the run's commit, not the built one. The tag, the smoke results and the digest echoed at the end must all describe the same commit, or the digest handed to the deploy slice would name something other than what was tested. This is the one place the fix is not purely additive, and it is a no-op on the existing paths.Why the promotion guard moved to the built ref. A dispatch from
livecarryingref: epic/LD3builds epic code; with the old guard (GITHUB_REF=refs/heads/live) it would have movedlatestonto an unmerged epic. The header's promotion rule sayslatestonly moves fromlive, and this makes that statement true of the built ref rather than of the run's ref.Verification
python3 -c "yaml.safe_load(...)"— parses; triggers/inputs inspected programmatically.run:block (GH expressions substituted like the runner does) passesbash -n; the new/changed blocks passshellcheck -S warningclean.yamllintfinding count unchanged at 67 — the file's pre-existing line-length/style level; no finding of a new class.secrets.,branches:,paths:,runs-on:orenv.IMAGE(the only addedenv:keys areBUILD_REFandDISPATCH_REF).What this does NOT do
latest(only alivebuild can).Land order (LD3.1b)
proddeploy PR withPIN_AFTER_BUILDreplaced by that digest →operatorgrant PR (infra-corecore/terraform/live/zitadel/) + KV seeding (human).The deploy slice (LD3.1b) pins a DIGEST, so the digest has to exist before the manifests can name it — and the cockpit image workflow's only push trigger is `live`, which the epic has not reached. This adds the dispatch path that publishes an epic/LD3 build. ADDITIVE, by construction: - `workflow_dispatch` gains one optional `ref` input (default "" = build the ref the dispatch ran on, i.e. the branch picked in the UI). A new step in BOTH jobs checks out a non-empty `ref` with a shallow single-ref fetch and `--detach`, so preflight answers "is the cockpit on THIS ref?" for the ref that will actually be built. - `SHORT_SHA` now comes from `git rev-parse HEAD` instead of `${{ github.sha }}`. Same seven characters on the push/PR paths (checkout HEAD IS github.sha there); on the dispatch path it is the BUILT commit, so the tag, the smoke results and the reported digest all describe one commit. - The `latest` promotion decision reads the BUILT ref rather than the run's ref. With no input the two strings are identical, so a `push` to `live` still promotes and nothing else does; the case this closes is a dispatch from `live` whose `ref` input names an epic branch, which must never move `latest`. Unchanged: both existing triggers byte-for-byte (branches/paths), the `if: github.event_name != 'pull_request'` push gate, the scoped HARBOR_PUSH_USERNAME/PASSWORD robot (no admin credential, no new secret), the SHA-tag-only push, and the size gate. Verified: YAML parses (PyYAML); every `run:` block passes `bash -n`; yamllint finding count unchanged at the file's pre-existing level (67 line-length/style findings before and after, none of a new class).Landing review — verdict: APPROVE-WITH-CONCERNS (no blockers)
Reviewed by a fresh, independent reviewer (separate process, primary sources only: the PR diff, both full workflow revisions via
git show, the ticket acceptance criteria). Head sha reviewed:9a874cb296d74f34ee3b754261276d53ac986c5f. Combined commit status on that sha: success (9 statuses, incl.CI Review / cockpit-e2e,dispatcher-test,cockpit-test,Build Lab Director Cockpit Image / build-and-push).Criteria check — all PASS
1 file changed, 100 insertions(+), 6 deletions(-); parsedondict equal to before exceptworkflow_dispatch; jobneeds/if/runs-onidentical, step lists differ only by the added dispatch step in each jobsecrets.refs still exactlyHARBOR_PUSH_USERNAME/HARBOR_PUSH_PASSWORD; newenv:keys are onlyBUILD_REFandDISPATCH_REFon.pushbyte-equal; on push/PRgithub.event.inputsis absent →BUILD_REF="${GITHUB_REF}"→refs/heads/livepromotes, PR refs fall to*); nodocker tag/docker pushother than:${SHORT_SHA}and the:latestaliasgithub.event.inputs.refproven to resolve through Forgejoworkflow_dispatch→ runnerEventpayload; empty input = documentedexit 0no-op; non-empty = shallow fetch +checkout --detach FETCH_HEADbeforeSet image variables; nonexistent ref exits 128 loudlySHORT_SHA/latest-guard changes justifiedWhy the two non-additive lines are justified.
SHORT_SHAhad to move togit rev-parse HEAD: the runner setsgithub.shafromEvent["after"](absent for a dispatch) and otherwise from the run's ref, never from therefinput — so with the old expression the tag, the smoke results and the read-back digest would describe a different commit than the one built. Thelatestguard had to key on the built ref: a dispatch attached torefs/heads/livecarryingref: epic/LD3would otherwise have movedlatestonto unmerged epic code.Concerns (non-blocking, posted for the record)
git fetch --depth=1 origin "${BUILD_REF}"passes dispatch input without--. Arefbeginning with-is parsed by git as an option (--all→fatal: fetch --all does not take a repository argument) and a multi-entryFETCH_HEADthen makescheckout --detach FETCH_HEADsucceed, so a dash-leading input buildsFETCH_HEAD's first entry instead of the named ref. Plain bogus refs still exit 128 loudly, and dispatch requires repo write permission, so this is hardening, not escalation:git fetch --depth=1 origin -- "${BUILD_REF}"or agit check-ref-formatguard.main), so it will not appear in the Actions UI picker on Forgejo 14.0.3 — dispatch is reachable only through the API (GetWorkflowFromCommit(git, opt.Ref, name)reads the file from the supplied ref). That is the intended route; worth a line in the runbook so nobody hunts for a button.refinput islivewill promotelatest. Correct by construction (the built commit is live's head), but it is a newly available human-triggered route tolatest; the header prose could say so.No blocker findings → proceeding to merge per the landing contract.
Landed + GitOps continuation
65e357ff9a5fcd132edc2bb31b597408edefa621(nowepic/LD3head), branch deleted.POST .../actions/workflows/cockpit-image-build.yaml/dispatches{"ref":"epic/LD3"}→ 204.workflow_dispatch,refs/heads/epic/LD3) → success,preflight4s +build-and-push1m23s. Log:no ref input: building 65e357ff9a…→Building …cockpit:65e357f→Image size OK.→ smokePASS→ref refs/heads/epic/LD3 is not live; …:latest untouched→Pushed: …cockpit@sha256:a248f426….Digest to pin in the 3.1b deploy slice (PR #878):
tag:
harbor.aaron.reynoza.org/lab-director/cockpit:65e357fcrane verification:
crane digest …:65e357f→sha256:a248f426…(matches the log);crane ls→ exactly one tag65e357f;crane digest …:latest→ 404 not found (crane lshas nolatest) — nothing created or moved; imagecreated2026-10-02T07:13:15Z, inside the run window.