HOMELAB-2479: ci(emu): gate emulator builds on their own inputs + pin both bases #887

Merged
pi-landing merged 2 commits from plane/HOMELAB-2479-emulator-gate into live 2026-11-07 12:15:21 +00:00
Owner

Why (measured, 2026-11-07T03:35–03:50Z)

The emulator image is the single biggest thing this registry is asked to store, and part of that is a trigger bug, not a real change.

fact object · field value
emulator pushes since the images were created crane ls harbor.aaron.reynoza.org/platform/android-emulator 5 tags (5c1407d 10-30, 3e59ad7 10-31, 39725f6 11-02, 230756a 11-02, e54f03b 11-07) over 7.82 d = 0.64 builds/day
one emulator push Harbor registry.artifact.size (platform/android-emulator, 24 h window) 2,705,462,010 B = 2.52 GiB
today's growth kubelet_volume_stats_used_bytes{pvc="harbor-registry"} delta(...[24h]) +4,402,770,852 B = 4.10 GiB/day
who grew it in the last 24 h Harbor registry.artifact.push_time > 24 h, sum size 10 artifacts / 4,449,717,100 B, of which platform/android-emulator 1 artifact 2.52 GiB (61 %), docker-cache/diegosouzapw (omniroute proxy fill) 6 artifacts 1.56 GiB, platform/docs 2 artifacts 51 MiB
free now kubelet_volume_stats_available_bytes 24,669,995,008 B = 22.98 GiB → 22.98 / 4.10 = 5.6 days

The 2.52 GiB in the last 24 h was pushed by e54f03b — a fixture-only commit (re-sync of the vendored sshd-fixture.sh). It changed no emulator input, and the emulator was rebuilt and pushed anyway, because the two images share one workflow and the workflow-level paths: filter cannot say which image a push affects. 3e59ad7 (2026-10-31) was the same story.

Of the 5 builds in the sample, 2 were fixture-only (3e59ad7, e54f03b), 2 emulator-only (39725f6, 230756a), 1 both (5c1407d, the commit that created both images).

What changes (3 files, +100/−3, two commits)

Commit 1 — gating (de08fa4).

  • A new changes job diffs the pushed range (push: github.event.before..github.sha; pull_request: base.sha..HEAD) and publishes two outputs.
  • platform/android-emulator/** → emulator=true only. platform/ghostty-ssh-fixture/** → fixture=true only. An image path wins, so a fixture fix that also edits the sha256 pin inside emu-images.yaml no longer drags the emulator along (that is exactly what e54f03b and 3e59ad7 did — every one of the 5 commits touched the workflow file).
  • emu-images.yaml alone, with no image path → both, so a workflow-only change still proves both images.
  • build-emulator / build-fixture gate on needs.changes.outputs.<image> == 'true'.
  • workflow_dispatch gains build_emulator (default false) and build_fixture (default true), as type: choice strings — a boolean dispatch input is coerced to a real JSON boolean and the engine's string-schema dispatch validation rejects it with 422 (GITS-314, measured in the app repo). The condition also matches the literal 'true', so a real boolean would still work.
  • Pull requests still build the affected image(s) with no credentials and still never push (if: github.event_name != 'pull_request' on both push steps is untouched).

Dry run of the shipped decision block against the 5 real live commits (the exact case statement that runs in CI):

5c1407d  emulator=true  fixture=true   (created both)
3e59ad7  emulator=false fixture=true   (fixture re-sync)   <- 2.52 GiB saved
39725f6  emulator=true  fixture=false  (emulator adb fix)
230756a  emulator=true  fixture=false  (emulator adb fix v2)
e54f03b  emulator=false fixture=true   (fixture re-sync)   <- 2.52 GiB saved

Commit 2 — base pins (9d1ef2b), separate so it can be dropped alone. Both Dockerfiles were FROM debian:bookworm-slim — a floating tag, so an upstream debian rebuild changes the base of every layer below it with no commit in this repo. Pinned to the multi-arch index digest read with crane digest at 03:43Z:

debian:bookworm-slim@sha256:734a72b3d6142a71993b4bc580ca01d8ca687f77a2c422d96a961d3376b179bc

(amd64 child c67e92c8…; the index keeps the manifest list, so a non-amd64 build still resolves.) Those two FROM lines are the only floating base references in either Dockerfile; every other download (JDK, cmdline-tools, herdr, the vendored script) was already sha256-pinned.

Math: what the gate is worth

  • Avoided: 2 of 5 builds × 2.52 GiB = 5.04 GiB per 7.82 d = 0.64 GiB/day → ~19.6 GiB/month at the current cadence (emulator volume 48 GiB/month → ~28 GiB/month).
  • Free space: 22.98 GiB / 4.10 GiB/day = 5.6 days today; with the gate, 22.98 / (4.10 − 0.64) = 6.6 days. +1.0 day.
  • Honest asymmetry: only 2.52 GiB of the last day's 4.10 GiB was CI push. 1.56 GiB was docker-cache proxy fill driven by pulls, which this PR does not touch, and node content stores hold a copy of every image the registry does too. This is a rate cut, not a cure.

Dispatch: two examples

# fixture only — the default. No emulator build, no 2.5 GiB push.
curl -sS -X POST \
  --config <(printf 'header = "Authorization: token %s"\n' "$(cat ~/.forgejo-token)") \
  -H 'Content-Type: application/json' \
  https://forgejo.aaron.reynoza.org/api/v1/repos/Homelab/infra-core/actions/workflows/emu-images.yaml/dispatches \
  -d '{"ref":"live","inputs":{"build_emulator":"false","build_fixture":"true"}}'

# emulator only
curl -sS -X POST \
  --config <(printf 'header = "Authorization: token %s"\n' "$(cat ~/.forgejo-token)") \
  -H 'Content-Type: application/json' \
  https://forgejo.aaron.reynoza.org/api/v1/repos/Homelab/infra-core/actions/workflows/emu-images.yaml/dispatches \
  -d '{"ref":"live","inputs":{"build_emulator":"true","build_fixture":"false"}}'

(the header is fed through a 0600 curl config so the token never lands in argv — HOMELAB-2656/GITS-380.)

Consumers: does anything need a fresh emulator per push? No.

Every consumer pins a digest, so freshness does not matter to any of them:

consumer object · field pin verdict
GITS emu lane Homelab/ghostty-in-the-shell .forgejo/workflows/emu.yaml:73 EMU_IMAGE platform/android-emulator@sha256:66430940… (build 230756a, no tag) digest-pinned
GITS probes .forgejo/workflows/probe-emu.yaml:61, .forgejo/workflows/release-smoke.yaml:38 EMU_IMAGE @sha256:8b0a6b49… (= build 5c1407d, 10-30) digest-pinned (and stale)
sbx-gits Homelab/infra-core core/manifests/apps/sbx-gits/statefulset.yaml:1073 .spec.template.spec.containers[].image :230756a@sha256:66430940… tag and digest
fixture emu.yaml:86, probe-emu.yaml:71 FIXTURE_IMAGE ghostty-ssh-fixture:3e59ad7@sha256:31ee38da… tag and digest

The only floating reference anywhere is a human recipe in docs/gits-sbx/emulador-host.md:207 (crane ls … | tail -1) — documentation, not CI.

Known red on live, not introduced here

build-fixture failed on live at e54f03b (run #3333, 8 s) and no fixture tag was pushed for that commit (crane ls platform/ghostty-ssh-fixture = 3e59ad7, 5c1407d, gits23-sync-18bbafe7). The failure is in the step Verify the control API at runtime (add-key + ssh login): after add-key succeeds, the step's ssh -p "${FIX_PORT}" prints Bad port '', i.e. sshd-fixture.sh env returned no FIX_PORT for a container the entrypoint started without FIX_SESSION_NAME — the re-synced shard's fixture discovery (stop / env) section (platform/ghostty-ssh-fixture/fixtures/sshd-fixture.sh:157+) does not find the session the entrypoint started. Symptom and location are measured; the root cause is unproven (would need the run's full log around discovery, which the 8 s failure excerpt does not carry). It will make this PR's own build-fixture red, exactly as it is red on live today. Filed as the next step of HOMELAB-2479, not fixed here — this PR is deliberately trigger-shaped only.

Rollback

git revert 9d1ef2b && git revert de08fa4 (or git revert of the range) — no state, no data, no retention or registry change is involved, so a revert restores today's behaviour exactly.

Open questions (for the reviewer and the next ticket)

  1. GITS re-pin list. The three GITS pins are stale: probe-emu.yaml:61 / release-smoke.yaml:38 pin 8b0a6b49… (5c1407d, 2026-10-30), while emu.yaml:73 and sbx-gits pin 66430940… (230756a, 2026-11-02). Re-pinning the two to 230756a/e54f03b is what makes the old emulator artifacts reclaimable at all (retention cannot delete a pinned digest). That is the sequenced next read, not this PR.
  2. Tradeoff to accept or reject: a commit that edits an emulator verify step inside emu-images.yaml while also touching a fixture path will build only the fixture. The next emulator-path change proves it, and build_emulator=true forces it by hand. If that is too loose, the alternative is a hunk-range test on the workflow file — a bigger diff than this ticket allows.
  3. $GITHUB_OUTPUT / job outputs: are used by the new changes job. The runner is current, but the first run of this PR is the proof (::set-output is the fallback form if the engine refuses it).
  4. Not touched: the apt-get install package versions in both Dockerfiles are still unpinned (a re-pin of those needs a version sweep, not a one-line change).

No merge. Reviewer first.

## Why (measured, 2026-11-07T03:35–03:50Z) The emulator image is the single biggest thing this registry is asked to store, and part of that is a trigger bug, not a real change. | fact | object · field | value | |---|---|---| | emulator pushes since the images were created | `crane ls harbor.aaron.reynoza.org/platform/android-emulator` | **5 tags** (`5c1407d` 10-30, `3e59ad7` 10-31, `39725f6` 11-02, `230756a` 11-02, `e54f03b` 11-07) over 7.82 d = **0.64 builds/day** | | one emulator push | Harbor `registry.artifact.size` (`platform/android-emulator`, 24 h window) | **2,705,462,010 B = 2.52 GiB** | | today's growth | `kubelet_volume_stats_used_bytes{pvc="harbor-registry"}` `delta(...[24h])` | **+4,402,770,852 B = 4.10 GiB/day** | | who grew it in the last 24 h | Harbor `registry.artifact.push_time` > 24 h, sum `size` | **10 artifacts / 4,449,717,100 B**, of which **`platform/android-emulator` 1 artifact 2.52 GiB (61 %)**, `docker-cache/diegosouzapw` (omniroute proxy fill) 6 artifacts 1.56 GiB, `platform/docs` 2 artifacts 51 MiB | | free now | `kubelet_volume_stats_available_bytes` | **24,669,995,008 B = 22.98 GiB** → 22.98 / 4.10 = **5.6 days** | The 2.52 GiB in the last 24 h was pushed by **`e54f03b` — a fixture-only commit** (re-sync of the vendored `sshd-fixture.sh`). It changed no emulator input, and the emulator was rebuilt and pushed anyway, because the two images share one workflow and the workflow-level `paths:` filter cannot say which image a push affects. `3e59ad7` (2026-10-31) was the same story. Of the 5 builds in the sample, **2 were fixture-only** (`3e59ad7`, `e54f03b`), **2 emulator-only** (`39725f6`, `230756a`), **1 both** (`5c1407d`, the commit that created both images). ## What changes (3 files, +100/−3, two commits) **Commit 1 — gating (`de08fa4`).** - A new `changes` job diffs the pushed range (`push`: `github.event.before..github.sha`; `pull_request`: `base.sha..HEAD`) and publishes two outputs. - `platform/android-emulator/**` → `emulator=true` only. `platform/ghostty-ssh-fixture/**` → `fixture=true` only. **An image path wins**, so a fixture fix that also edits the sha256 pin inside `emu-images.yaml` no longer drags the emulator along (that is exactly what `e54f03b` and `3e59ad7` did — every one of the 5 commits touched the workflow file). - `emu-images.yaml` alone, with no image path → both, so a workflow-only change still proves both images. - `build-emulator` / `build-fixture` gate on `needs.changes.outputs.<image> == 'true'`. - `workflow_dispatch` gains `build_emulator` (**default `false`**) and `build_fixture` (**default `true`**), as `type: choice` strings — a boolean dispatch input is coerced to a real JSON boolean and the engine's string-schema dispatch validation rejects it with 422 (GITS-314, measured in the app repo). The condition also matches the literal `'true'`, so a real boolean would still work. - Pull requests still build the affected image(s) with no credentials and still never push (`if: github.event_name != 'pull_request'` on both push steps is untouched). Dry run of the shipped decision block against the 5 real live commits (the exact `case` statement that runs in CI): ``` 5c1407d emulator=true fixture=true (created both) 3e59ad7 emulator=false fixture=true (fixture re-sync) <- 2.52 GiB saved 39725f6 emulator=true fixture=false (emulator adb fix) 230756a emulator=true fixture=false (emulator adb fix v2) e54f03b emulator=false fixture=true (fixture re-sync) <- 2.52 GiB saved ``` **Commit 2 — base pins (`9d1ef2b`), separate so it can be dropped alone.** Both Dockerfiles were `FROM debian:bookworm-slim` — a floating tag, so an upstream debian rebuild changes the base of every layer below it with no commit in this repo. Pinned to the multi-arch index digest read with `crane digest` at 03:43Z: ``` debian:bookworm-slim@sha256:734a72b3d6142a71993b4bc580ca01d8ca687f77a2c422d96a961d3376b179bc ``` (amd64 child `c67e92c8…`; the index keeps the manifest list, so a non-amd64 build still resolves.) Those two `FROM` lines are the only floating base references in either Dockerfile; every other download (JDK, cmdline-tools, herdr, the vendored script) was already sha256-pinned. ## Math: what the gate is worth - Avoided: 2 of 5 builds × 2.52 GiB = **5.04 GiB per 7.82 d = 0.64 GiB/day → ~19.6 GiB/month** at the current cadence (emulator volume 48 GiB/month → ~28 GiB/month). - Free space: 22.98 GiB / 4.10 GiB/day = **5.6 days** today; with the gate, 22.98 / (4.10 − 0.64) = **6.6 days**. **+1.0 day.** - Honest asymmetry: only 2.52 GiB of the last day's 4.10 GiB was CI push. 1.56 GiB was `docker-cache` proxy fill driven by **pulls**, which this PR does not touch, and node content stores hold a copy of every image the registry does too. This is a rate cut, not a cure. ## Dispatch: two examples ``` # fixture only — the default. No emulator build, no 2.5 GiB push. curl -sS -X POST \ --config <(printf 'header = "Authorization: token %s"\n' "$(cat ~/.forgejo-token)") \ -H 'Content-Type: application/json' \ https://forgejo.aaron.reynoza.org/api/v1/repos/Homelab/infra-core/actions/workflows/emu-images.yaml/dispatches \ -d '{"ref":"live","inputs":{"build_emulator":"false","build_fixture":"true"}}' # emulator only curl -sS -X POST \ --config <(printf 'header = "Authorization: token %s"\n' "$(cat ~/.forgejo-token)") \ -H 'Content-Type: application/json' \ https://forgejo.aaron.reynoza.org/api/v1/repos/Homelab/infra-core/actions/workflows/emu-images.yaml/dispatches \ -d '{"ref":"live","inputs":{"build_emulator":"true","build_fixture":"false"}}' ``` (the header is fed through a 0600 curl config so the token never lands in argv — HOMELAB-2656/GITS-380.) ## Consumers: does anything need a fresh emulator per push? No. Every consumer pins a **digest**, so freshness does not matter to any of them: | consumer | object · field | pin | verdict | |---|---|---|---| | GITS `emu` lane | `Homelab/ghostty-in-the-shell` `.forgejo/workflows/emu.yaml:73` `EMU_IMAGE` | `platform/android-emulator@sha256:66430940…` (build `230756a`, no tag) | digest-pinned | | GITS probes | `.forgejo/workflows/probe-emu.yaml:61`, `.forgejo/workflows/release-smoke.yaml:38` `EMU_IMAGE` | `@sha256:8b0a6b49…` (= build `5c1407d`, 10-30) | digest-pinned (and stale) | | `sbx-gits` | `Homelab/infra-core` `core/manifests/apps/sbx-gits/statefulset.yaml:1073` `.spec.template.spec.containers[].image` | `:230756a@sha256:66430940…` | tag **and** digest | | fixture | `emu.yaml:86`, `probe-emu.yaml:71` `FIXTURE_IMAGE` | `ghostty-ssh-fixture:3e59ad7@sha256:31ee38da…` | tag **and** digest | The only floating reference anywhere is a human recipe in `docs/gits-sbx/emulador-host.md:207` (`crane ls … | tail -1`) — documentation, not CI. ## Known red on `live`, not introduced here `build-fixture` failed on `live` at `e54f03b` (run #3333, 8 s) and **no fixture tag was pushed for that commit** (`crane ls platform/ghostty-ssh-fixture` = `3e59ad7`, `5c1407d`, `gits23-sync-18bbafe7`). The failure is in the step `Verify the control API at runtime (add-key + ssh login)`: after `add-key` succeeds, the step's `ssh -p "${FIX_PORT}"` prints `Bad port ''`, i.e. `sshd-fixture.sh env` returned no `FIX_PORT` for a container the entrypoint started without `FIX_SESSION_NAME` — the re-synced shard's `fixture discovery (stop / env)` section (`platform/ghostty-ssh-fixture/fixtures/sshd-fixture.sh:157+`) does not find the session the entrypoint started. Symptom and location are measured; the root cause is **unproven** (would need the run's full log around discovery, which the 8 s failure excerpt does not carry). It will make this PR's own `build-fixture` red, exactly as it is red on `live` today. Filed as the next step of HOMELAB-2479, not fixed here — this PR is deliberately trigger-shaped only. ## Rollback `git revert 9d1ef2b && git revert de08fa4` (or `git revert` of the range) — no state, no data, no retention or registry change is involved, so a revert restores today's behaviour exactly. ## Open questions (for the reviewer and the next ticket) 1. **GITS re-pin list.** The three GITS pins are stale: `probe-emu.yaml:61` / `release-smoke.yaml:38` pin `8b0a6b49…` (`5c1407d`, 2026-10-30), while `emu.yaml:73` and `sbx-gits` pin `66430940…` (`230756a`, 2026-11-02). Re-pinning the two to `230756a`/`e54f03b` is what makes the old emulator artifacts reclaimable at all (retention cannot delete a pinned digest). That is the sequenced next read, not this PR. 2. **Tradeoff to accept or reject:** a commit that edits an *emulator* verify step inside `emu-images.yaml` while also touching a fixture path will build only the fixture. The next emulator-path change proves it, and `build_emulator=true` forces it by hand. If that is too loose, the alternative is a hunk-range test on the workflow file — a bigger diff than this ticket allows. 3. **`$GITHUB_OUTPUT` / job `outputs:`** are used by the new `changes` job. The runner is current, but the first run of this PR is the proof (`::set-output` is the fallback form if the engine refuses it). 4. **Not touched:** the `apt-get install` package versions in both Dockerfiles are still unpinned (a re-pin of those needs a version sweep, not a one-line change). **No merge.** Reviewer first.
The two images share one workflow but have independent inputs, so the
workflow-level path filter cannot say WHICH image a push affects: every
commit that touched the fixture also rebuilt and pushed the 2.52 GiB
emulator (measured: platform/android-emulator tags 3e59ad7 2026-10-31 and
e54f03b 2026-11-07T02:36Z; artifact size 2,705,462,010 B).

- new 'changes' job: diffs the pushed range (push: event.before..sha,
  pull_request: base.sha..HEAD) and sets per-image outputs; a path under
  platform/android-emulator/ builds only the emulator, a path under
  platform/ghostty-ssh-fixture/ only the fixture, and this workflow file
  alone builds both. An image path wins, so a fixture fix that also edits
  the sha256 pin in this file does not drag the emulator along.
- build-emulator / build-fixture gate on those outputs (needs: changes).
- workflow_dispatch gains build_emulator (default false) and
  build_fixture (default true), as choice strings: a boolean dispatch
  input is coerced to a real JSON boolean and rejected with 422 by the
  engine's string-schema dispatch validation (GITS-314).

Pull requests still build the affected image(s) with no credentials and
never push. Rollback: revert this commit.
HOMELAB-2479: ci(emu): pin both base images to the debian digest
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / runner-manifest-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 11s
Build Emulator + SSH Fixture Images / changes (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 22s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 21s
CI Review / record-sql-test (pull_request) Successful in 50s
Build Emulator + SSH Fixture Images / build-fixture (pull_request) Successful in 1m28s
CI Review / cockpit-test (pull_request) Successful in 2m40s
Lint & Validate / terraform-validate (pull_request) Successful in 2m41s
CI Review / cockpit-e2e (pull_request) Successful in 3m29s
CI Review / dispatcher-test (pull_request) Successful in 4m39s
Build Emulator + SSH Fixture Images / build-emulator (pull_request) Successful in 5m39s
9d1ef2b48c
Both emulator Dockerfiles start FROM debian:bookworm-slim — a floating tag,
so an upstream debian rebuild changes the base of every layer here with no
commit in this repo. Pin to the multi-arch index digest read at
2026-11-07T03:43Z:

  debian:bookworm-slim@sha256:734a72b3d6142a71993b4bc580ca01d8ca687f77a2c422d96a961d3376b179bc

(linux/amd64 child c67e92c8099ba22853ab4cfda7cfb494d253d6012ca82dee98ba8976772af018;
the index keeps the multi-arch manifest list, so a non-amd64 build still
resolves.)

Separate commit from the gating change on purpose: drop or revert this one
alone if the re-pin needs scheduling. Also unpinned and NOT touched here:
the apt-get packages in both Dockerfiles. Rollback: revert this commit.
Collaborator

Verdict: APPROVE

9d1ef2b48c
model: omniroute/deepseek-flash
session: 01a1b5c8-851e-76f0-828e-7f80b7d1a776

Seat 1 of 2 (independent; wrote this without reading the other seat).

Verified (measured, re-run myself)

  • Dry-run table: 5/5 correct. I ran the shipped case block against git diff --name-only <c>^ <c> for all five commits: 5c1407d emu=T fix=T · 3e59ad7 emu=F fix=T · 39725f6 emu=T fix=F · 230756a emu=T fix=F · e54f03b emu=F fix=T. All five are merge commits, so per-commit diff equals the push range before..sha.
  • Digest pin: exact. crane digest debian:bookworm-slim today = sha256:734a72b3…b179bc — the pinned value. Multi-arch claim holds: amd64 child sha256:c67e92c8…, arm64 sha256:ec8534cd…, so the index digest keeps non-amd64 builds resolvable.
  • Floating bases: exactly one FROM per Dockerfile, both pinned; nothing else floats inside those two files.
  • Safety intact: both docker push and both crane digest steps still carry if: github.event_name != 'pull_request' (lines 284, 302, 419, 431); HARBOR_PUSH_* appears only inside them. PR builds push nothing.
  • CI green on this exact head: changes ✓ 9s, build-emulator ✓ 5m39s, build-fixture ✓ 1m28s; yaml-lint, shellcheck, terraform-validate and the unit/e2e workflow also ✓. That also settles the PR's own open question 3: $GITHUB_OUTPUT + job outputs: work, because an absent output would have skipped both gated jobs.
  • Consumers: core/manifests/apps/sbx-gits/statefulset.yaml pins :230756a@sha256:66430940… — at line 1084, not 1073. Only floating reference is the docs recipe docs/gits-sbx/emulador-host.md:207 (documentation, not CI). GITS-repo pins: unknown, I did not read Homelab/ghostty-in-the-shell.

Concerns

  • Precedence hole (your Q2): one push that edits an emulator verify step inside emu-images.yaml and any fixture path builds only the fixture — a broken emulator verify step can land on live unproven. Bounded (build_emulator=true forces it), so acceptable, but it should be written down where the next editor will read it.
  • Empty range builds nothing, green: if changed is empty, emu and fix both stay false and both jobs are skipped — the run is green with zero builds and reads as a pass unless you open the job list. A [ -z "$changed" ] && build both guard closes it.
  • The classifier fails closed: set -euo pipefail with an unresolvable before (force-push rewriting live) turns changes red and skips both builds. Classifier error ⇒ build both is the safer default.
  • Two-dot pull_request range: "${BASE_SHA}..HEAD" is a tree comparison. Measured on this very branch, base..head lists 12 files (it shows live-only commits as reversions). Over-build only, so fail-safe — "${BASE_SHA}...HEAD" states the intent without depending on branch freshness.
  • Re-pin burden: no renovate/dependabot in this repo, so a digest pin trades silent drift for staleness with no owner. Sequence it.
  • Claim honesty: "will make this PR's own build-fixture red" did not reproduce (✓ 1m28s). Consistent with the PR run building the merge ref, which carries live's #886 FIX_PORT gate that this branch's merge-base lacks; since the failure is timing-dependent, the non-reproduction proves nothing either way. Safe: the PR diff does not touch that verify step, so the merge result is unaffected.

Suggestions

  • Revert path is clean (no state, no retention, no registry change); only residue is dispatch callers finding build_emulator gone.
  • fetch-depth: 0 full clone per run on the k8s pool, in a ticket about runner/storage saturation — worth a shallow alternative.

Evidence: ~/repos/homelab/working-files/infra-stability/evidence-20261107/seat1-887.md

## Verdict: APPROVE 9d1ef2b48c07fbb028e3bccbc4ade45e0ad01687 model: omniroute/deepseek-flash session: 01a1b5c8-851e-76f0-828e-7f80b7d1a776 Seat 1 of 2 (independent; wrote this without reading the other seat). ### Verified (measured, re-run myself) - **Dry-run table: 5/5 correct.** I ran the shipped `case` block against `git diff --name-only <c>^ <c>` for all five commits: 5c1407d emu=T fix=T · 3e59ad7 emu=F fix=T · 39725f6 emu=T fix=F · 230756a emu=T fix=F · e54f03b emu=F fix=T. All five are merge commits, so per-commit diff equals the push range `before..sha`. - **Digest pin: exact.** `crane digest debian:bookworm-slim` today = `sha256:734a72b3…b179bc` — the pinned value. Multi-arch claim holds: amd64 child `sha256:c67e92c8…`, arm64 `sha256:ec8534cd…`, so the index digest keeps non-amd64 builds resolvable. - **Floating bases:** exactly one `FROM` per Dockerfile, both pinned; nothing else floats inside those two files. - **Safety intact:** both `docker push` and both `crane digest` steps still carry `if: github.event_name != 'pull_request'` (lines 284, 302, 419, 431); `HARBOR_PUSH_*` appears only inside them. PR builds push nothing. - **CI green on this exact head:** `changes` ✓ 9s, `build-emulator` ✓ 5m39s, `build-fixture` ✓ 1m28s; yaml-lint, shellcheck, terraform-validate and the unit/e2e workflow also ✓. That also settles the PR's own open question 3: `$GITHUB_OUTPUT` + job `outputs:` work, because an absent output would have skipped both gated jobs. - **Consumers:** `core/manifests/apps/sbx-gits/statefulset.yaml` pins `:230756a@sha256:66430940…` — at line **1084**, not 1073. Only floating reference is the docs recipe `docs/gits-sbx/emulador-host.md:207` (documentation, not CI). GITS-repo pins: unknown, I did not read `Homelab/ghostty-in-the-shell`. ### Concerns - **Precedence hole (your Q2):** one push that edits an *emulator* verify step inside `emu-images.yaml` **and** any fixture path builds only the fixture — a broken emulator verify step can land on live unproven. Bounded (`build_emulator=true` forces it), so acceptable, but it should be written down where the next editor will read it. - **Empty range builds nothing, green:** if `changed` is empty, `emu` and `fix` both stay false and both jobs are skipped — the run is green with zero builds and reads as a pass unless you open the job list. A `[ -z "$changed" ] && build both` guard closes it. - **The classifier fails closed:** `set -euo pipefail` with an unresolvable `before` (force-push rewriting live) turns `changes` red and skips both builds. Classifier error ⇒ build both is the safer default. - **Two-dot `pull_request` range:** `"${BASE_SHA}..HEAD"` is a tree comparison. Measured on this very branch, base..head lists 12 files (it shows live-only commits as reversions). Over-build only, so fail-safe — `"${BASE_SHA}...HEAD"` states the intent without depending on branch freshness. - **Re-pin burden:** no renovate/dependabot in this repo, so a digest pin trades silent drift for staleness with no owner. Sequence it. - **Claim honesty:** "will make this PR's own `build-fixture` red" did not reproduce (✓ 1m28s). Consistent with the PR run building the merge ref, which carries live's #886 `FIX_PORT` gate that this branch's merge-base lacks; since the failure is timing-dependent, the non-reproduction proves nothing either way. Safe: the PR diff does not touch that verify step, so the merge result is unaffected. ### Suggestions - Revert path is clean (no state, no retention, no registry change); only residue is dispatch callers finding `build_emulator` gone. - `fetch-depth: 0` full clone per run on the `k8s` pool, in a ticket about runner/storage saturation — worth a shallow alternative. Evidence: `~/repos/homelab/working-files/infra-stability/evidence-20261107/seat1-887.md`
Collaborator

Verdict: APPROVE

9d1ef2b48c
model: omniroute/deepseek-flash
session: 01a1b5d0-986b-76f0-828e-7f86ed9ed4ce

Adversarial/threat-model pass, primary sources only. No hold-level finding. Evidence: working-files/infra-stability/evidence-20261107/seat2-887.md.

Verified

  • crane digest debian:bookworm-slim today = sha256:734a72b3…b179bc, exactly the pin; amd64 child c67e92c8… matches. Both Dockerfiles are single-stage (one FROM each), so no second floating build stage.
  • Dispatch: all four input combinations resolve as intended; defaults false/true match the body's examples.
  • $GITHUB_OUTPUT (open question 3) is proven, not hypothetical: had the writes been refused, set -u would have aborted the step and both gated jobs would have been skipped. Run #3338 changes succeeded in 9 s and both built.
  • Merge probe (git merge origin/live, then aborted): clean. The merged tree keeps the fixture sha256 identical at all three sites (workflow constant, Dockerfile ARG, file) and preserves #886's non-empty-FIX_PORT gate and 601a425c's unique container name. No pin conflict, no re-introduced broken script.

concern — changes failure fails to nothing. A bad range, checkout failure or force-pushed before makes git diff exit 128; the step dies under set -e, outputs stay empty, and needs: changes skips both builds. Whether that surfaces as a missing required check is unknown: GET /branch_protections is refused for this credential. Missing evidence: that API as an admin, and one run with a deliberately bad range.

concern — the PR range is two-dot. base.sha..HEAD is a tree diff, not "what the PR changed": on this head it lists 12 files (body says 3), including -259 lines of a sshd-fixture.sh the PR never touches. Over-report on PRs costs builds only. The permissive direction is measured: --name-only prints only a rename's destination, so moving an input out of an image dir under-reports and skips a needed rebuild.

concern — the body's "known red on live" is stale. Live #3346 (#886) shows build-fixture success 1m8s; the red run was #3333, and #886 postdates the branch point. This head carries the other script version, so the prediction is wrong both ways. Green here proves the gate, not live's fixture job.

concern — actions/checkout@v4 is now the only floating reference in this workflow (a fourth use is added here), while every download and both bases are pinned.

suggestion A false/false dispatch is a successful no-op run; consider refusing it. suggestion State a debian pin-bump cadence, or the fresh pin rots silently; the unpinned apt-get versions (open question 4) are the largest remaining float, correctly out of scope.

Open questions 1: agree it is the sequenced next read. 2: accept — forcing both on any workflow edit would cost 2.5 GiB on 5 of 5 sampled commits; the dispatch input is a real escape hatch. 3: answered above. 4: out of scope.

## Verdict: APPROVE 9d1ef2b48c07fbb028e3bccbc4ade45e0ad01687 model: omniroute/deepseek-flash session: 01a1b5d0-986b-76f0-828e-7f86ed9ed4ce Adversarial/threat-model pass, primary sources only. No hold-level finding. Evidence: `working-files/infra-stability/evidence-20261107/seat2-887.md`. **Verified** - `crane digest debian:bookworm-slim` today = `sha256:734a72b3…b179bc`, exactly the pin; amd64 child `c67e92c8…` matches. Both Dockerfiles are single-stage (one `FROM` each), so no second floating build stage. - Dispatch: all four input combinations resolve as intended; defaults `false`/`true` match the body's examples. - `$GITHUB_OUTPUT` (open question 3) is proven, not hypothetical: had the writes been refused, `set -u` would have aborted the step and both gated jobs would have been skipped. Run #3338 `changes` succeeded in 9 s and both built. - Merge probe (`git merge origin/live`, then aborted): clean. The merged tree keeps the fixture sha256 identical at all three sites (workflow constant, Dockerfile ARG, file) and preserves #886's non-empty-`FIX_PORT` gate and 601a425c's unique container name. No pin conflict, no re-introduced broken script. **concern — `changes` failure fails to nothing.** A bad range, checkout failure or force-pushed `before` makes `git diff` exit 128; the step dies under `set -e`, outputs stay empty, and `needs: changes` skips both builds. Whether that surfaces as a missing required check is unknown: `GET /branch_protections` is refused for this credential. Missing evidence: that API as an admin, and one run with a deliberately bad range. **concern — the PR range is two-dot.** `base.sha..HEAD` is a tree diff, not "what the PR changed": on this head it lists 12 files (body says 3), including `-259` lines of a `sshd-fixture.sh` the PR never touches. Over-report on PRs costs builds only. The permissive direction is measured: `--name-only` prints only a rename's destination, so moving an input out of an image dir under-reports and skips a needed rebuild. **concern — the body's "known red on live" is stale.** Live #3346 (#886) shows `build-fixture success 1m8s`; the red run was #3333, and #886 postdates the branch point. This head carries the other script version, so the prediction is wrong both ways. Green here proves the gate, not live's fixture job. **concern — `actions/checkout@v4`** is now the only floating reference in this workflow (a fourth use is added here), while every download and both bases are pinned. **suggestion** A `false`/`false` dispatch is a successful no-op run; consider refusing it. **suggestion** State a debian pin-bump cadence, or the fresh pin rots silently; the unpinned `apt-get` versions (open question 4) are the largest remaining float, correctly out of scope. **Open questions** 1: agree it is the sequenced next read. 2: accept — forcing both on any workflow edit would cost 2.5 GiB on 5 of 5 sampled commits; the dispatch input is a real escape hatch. 3: answered above. 4: out of scope.
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!887
No description provided.