forgejo role: enable Actions — pinned runner on the guest (issue #3) #5

Merged
pit merged 10 commits from forgejo-actions into main 2026-10-06 06:45:40 +00:00
Owner

Summary

forgejo role (prod)
├── app.ini.j2          + [actions] ENABLED, DEFAULT_ACTIONS_URL=data.forgejo.org
├── defaults            + runner v13.1.0 pin, container-only labels, actions repos
├── tasks               + §4 runner install/register/daemon, §6 per-repo has_actions
├── runner.yaml.j2      + server.connections (shared-secret auth, uuid derivation)
└── handlers             + restart forgejo runner

Runner executes jobs in containers (Docker inside the guest, nesting enabled) — verified live on PRE: a pushed smoke workflow ran green, its job in a container (Linux 7.0.14-19-pve ... 7a0fae0482a3), logs stored by the instance. Container-only labels: no host label is registered, so no workflow can bypass the boundary by asking for it. Decision + RCE mitigations in docs/adr/0001.

Registration is Forgejo 15's shared-secret flow: a vaulted 40-hex secret is upserted server-side (forgejo-cli actions register, idempotent — same row keyed on uuid=secret[:16]) and the daemon authenticates via server.connections. This deliberately supersedes the issue's sketched registration-token mechanism (deprecated in runner v13); reasoning in the ADR.

Per-repo has_actions is ensured via API with a transient admin token minted on the guest (create → PATCH only the repos that lack it → self-DELETE; nothing long-lived left behind), guarded by a read-only repo_unit SELECT so re-runs stay changed=0.

The PRE rehearsal rig lives on the forgejo-pre branch (a593463), not here — a disposable guest in the main tree would make every plan show its creation. README documents the checkout → apply -target → playbook → destroy flow.

Evidence

PRE rehearsal (LXC 142, built from the parked rig, since destroyed):

  • Before (run 1): runner daemon crash-looped unauthenticated: unregistered runner — config template got the PROD URL because inventory host-line vars lose to playbook group_vars.
    After (run 2): ok=26 changed=5 failed=0; runner declared, last_online heartbeating.
  • Idempotency: re-run changed=0 (verified after every review fix — 5 consecutive convergent runs total).
  • Drift repair: hand-broke 7 things (zeroed binary, deleted config+scratch dir, stopped service, removed docker group, disabled actions in app.ini) → re-run changed=7, all converged.
  • Smoke workflow: pit/smoke .forgejo/workflows/smoke.yml on runs-on: docker → run 1 success; hello from the runner in the instance-stored log; job executed in a container.
  • Vault leak check: decrypted values grepped against repo and worktree → 0 matches (convention 5).
  • Clean tree: PRE guest destroyed; tofu plan on this branch = No changes (no phantom container in every future plan).
  • PROD untouched so far: guest 141 gets this after merge — PBS snapshot first (convention 3) — and the trivial-workflow smoke test on PROD is the issue's closing criterion.

Review

Two-axis review (standards + spec) run pre-merge; material findings fixed in 3b6147e + a593463: ADR renumbered to 0001, README restates its own gates (plan review, PBS snapshot), magic repo_unit type hoisted to a named default, host label removed from registration, PATCH loop narrowed to the repos that lack the unit, query_result empty-dict normalization (live bug found on PRE), handler-order comment corrected. PR review (0800332): PRE rig parked off-main, ADR mention dropped from the tasks banner, ADR 0001: prefixed to the ADR title.

Merge Danger

Door: two-way (role change; rollback = revert + playbook re-run, runner removal is systemctl disable + delete binary).

Blast Radius: prod-guest service.

Adds Docker + a runner daemon to the prod guest (RAM headroom for job containers; the runner is RCE-by-design — container labels + single-user, registration-disabled instance are the mitigations, ADR-0001). The app.ini change restarts forgejo once at rollout. No data migrations; forgejo 15.0.9 ships actions GA. The vault.yml re-encryption adds the runner secret (same values otherwise).

## Summary ```text forgejo role (prod) ├── app.ini.j2 + [actions] ENABLED, DEFAULT_ACTIONS_URL=data.forgejo.org ├── defaults + runner v13.1.0 pin, container-only labels, actions repos ├── tasks + §4 runner install/register/daemon, §6 per-repo has_actions ├── runner.yaml.j2 + server.connections (shared-secret auth, uuid derivation) └── handlers + restart forgejo runner ``` Runner executes jobs **in containers** (Docker inside the guest, nesting enabled) — verified live on PRE: a pushed smoke workflow ran green, its job in a container (`Linux 7.0.14-19-pve ... 7a0fae0482a3`), logs stored by the instance. Container-only labels: no `host` label is registered, so no workflow can bypass the boundary by asking for it. Decision + RCE mitigations in `docs/adr/0001`. Registration is Forgejo 15's shared-secret flow: a vaulted 40-hex secret is upserted server-side (`forgejo-cli actions register`, idempotent — same row keyed on uuid=secret[:16]) and the daemon authenticates via `server.connections`. This deliberately supersedes the issue's sketched registration-token mechanism (deprecated in runner v13); reasoning in the ADR. Per-repo `has_actions` is ensured via API with a transient admin token minted on the guest (create → PATCH only the repos that lack it → self-DELETE; nothing long-lived left behind), guarded by a read-only `repo_unit` SELECT so re-runs stay `changed=0`. The PRE rehearsal rig lives on the `forgejo-pre` branch (a593463), not here — a disposable guest in the main tree would make every plan show its creation. README documents the checkout → apply -target → playbook → destroy flow. ## Evidence PRE rehearsal (LXC 142, built from the parked rig, since destroyed): - **Before (run 1):** runner daemon crash-looped `unauthenticated: unregistered runner` — config template got the PROD URL because inventory host-line vars lose to playbook group_vars. **After (run 2):** `ok=26 changed=5 failed=0`; runner declared, `last_online` heartbeating. - **Idempotency:** re-run `changed=0` (verified after every review fix — 5 consecutive convergent runs total). - **Drift repair:** hand-broke 7 things (zeroed binary, deleted config+scratch dir, stopped service, removed docker group, disabled actions in app.ini) → re-run `changed=7`, all converged. - **Smoke workflow:** `pit/smoke` `.forgejo/workflows/smoke.yml` on `runs-on: docker` → run 1 `success`; `hello from the runner` in the instance-stored log; job executed in a container. - **Vault leak check:** decrypted values grepped against repo and worktree → 0 matches (convention 5). - **Clean tree:** PRE guest destroyed; `tofu plan` on this branch = `No changes` (no phantom container in every future plan). - **PROD untouched so far:** guest 141 gets this after merge — PBS snapshot first (convention 3) — and the trivial-workflow smoke test on PROD is the issue's closing criterion. ## Review Two-axis review (standards + spec) run pre-merge; material findings fixed in 3b6147e + a593463: ADR renumbered to 0001, README restates its own gates (plan review, PBS snapshot), magic `repo_unit` type hoisted to a named default, `host` label removed from registration, PATCH loop narrowed to the repos that lack the unit, `query_result` empty-dict normalization (live bug found on PRE), handler-order comment corrected. PR review (0800332): PRE rig parked off-main, ADR mention dropped from the tasks banner, `ADR 0001:` prefixed to the ADR title. ## Merge Danger **Door:** two-way (role change; rollback = revert + playbook re-run, runner removal is `systemctl disable` + delete binary). **Blast Radius:** prod-guest service. Adds Docker + a runner daemon to the prod guest (RAM headroom for job containers; the runner is RCE-by-design — container labels + single-user, registration-disabled instance are the mitigations, ADR-0001). The app.ini change restarts forgejo once at rollout. No data migrations; forgejo 15.0.9 ships actions GA. The `vault.yml` re-encryption adds the runner secret (same values otherwise).
- app.ini template gains [actions] (ENABLED, DEFAULT_ACTIONS_URL=Forgejo's
  own mirror); rides the existing restart notify.
- runner v13.1.0 pinned in defaults (convention 4), downloaded checksum-first
  like the service binary, under the service home, service-user owned.
- Forgejo 15 shared-secret registration: vaulted 40-hex secret registered
  server-side via forgejo-cli actions register (idempotent upsert); daemon
  connects via server.connections (uuid = secret[:16] as forgejo derives it).
- systemd unit forgejo-runner.service: Restart=on-failure, PartOf=forgejo.
- jobs in containers (Docker in the LXC, nesting on): verified on PRE with
  a smoke workflow — green run, logs in the instance, job in a container.
- per-repo has_actions ensured via API with a transient admin token minted
  on the guest (create → PATCH → self-DELETE); guarded by a read-only
  repo_unit SELECT so re-runs stay changed=0.
- PRE guest as tofu-managed rehearsal rig (-target apply), host_vars
  overrides; playbook idempotence verified on PRE (run 3: changed=0) plus
  drift-repair convergence.
- ADR-0002 records the runner + container-label decision and its RCE
  mitigations.

PRE rehearsal transcript: run 1 converge (failed on a host-var precedence
bug — inventory host-line vars lose to playbook group_vars; fixed via
host_vars/), run 2 green (changed=5), run 3 idempotent (changed=0),
run 4 drift repair (changed=7, converged), run 5 changed=0.
- The wiki-mirror ADR lives only on an unmerged branch; per operator it is
  non-existent, so the Actions ADR is 0001 and the 'interaction with
  ADR-0001' section is gone.
- README: the PRE/Actions runbook now restates its own gates (tofu plan
  review, PBS snapshot before the Prod run).
- repo_unit magic number (10) hoisted to forgejo_actions_unit_type with the
  forgejo-source citation.

PRE re-verified after changes: changed=0.
- drop the live 'host' label: story 13 wants jobs in containers, and the
  ADR's own fallback wording says host execution must replace the decision,
  not coexist with it. A host label would let any workflow bypass the
  isolation boundary by asking for it; the ADR now records the reasoning.
- PATCH iterates the repos the guard found missing, not all configured
  repos (idempotence wobble with >1 repo otherwise).
- postgresql_query returns an empty dict on zero rows and loop templating
  runs before when: normalize with default([], true) (found live on PRE).
- flush_handlers comment now states the real mechanism: handlers run in
  definition order, not notification order.
- ADR notes the deliberate supersession of the spec's registration-token
  sketch (Forgejo 15 replaced that flow; runner v13 deprecates it).

PRE re-verified after each fix: changed=0, labels re-registered container-
only, runner active.
@ -119,3 +119,3 @@
# ============================================================
# 4. create accounts
# 4. enable Forgejo Actions + the runner (docs/adr/0001)
Author
Owner

remove adr mention

remove adr mention
pit marked this conversation as resolved
@ -0,0 +1,51 @@
# Enable Forgejo Actions: runner on the guest, jobs in containers
Author
Owner

Add ADR 0001: to the title

Add `ADR 0001:` to the title
pit marked this conversation as resolved
tofu/pre.tf Outdated
@ -0,0 +1,48 @@
# PRE guest: disposable Forgejo rehearsal container (LXC 142). Managed in the
# same tofu state as the prod guest; created/destroyed with -target as the
# role's test rig (issue #3: "the PRE run is the test rig").
resource "proxmox_virtual_environment_container" "forgejo_pre" {
Author
Owner

Do we want to commit the PRE resource? Doesn't this make every plan show the creation of the container?

Do we want to commit the PRE resource? Doesn't this make every plan show the creation of the container?
pit marked this conversation as resolved
A disposable guest in the main tree makes every plan show its creation —
exactly the plan noise convention 2 exists to prevent. The rig (tofu
resource, host_vars overrides, inventory host line) moves to the
forgejo-pre branch, following the repo's existing pattern for PRE config.
guest_pre stays in variables.tf (documented as parked-consumer) so a
rehearsal needs no variable re-adds; tofu validate passes without pre.tf.

Also: ADR mention dropped from the tasks banner; 'ADR 0001:' prefixed to
the ADR title.

PRE guest destroyed (state clean: plan on this branch = 'No changes').
pit left a comment

All three comments addressed in 0800332:

  1. tasks banner — ADR mention removed; the banner now reads # 4. enable Forgejo Actions + the runner.
  2. ADR title — now # ADR 0001: Enable Forgejo Actions: runner on the guest, jobs in containers.
  3. PRE resource out of main — you were right: with the guest destroyed, every plan showed +1 to add for it. The rig (tofu/pre.tf, host_vars/forgejo-pre.yml, the pre inventory host line) is parked on the forgejo-pre branch at a593463 — the same pattern the README already documented for PRE config. guest_pre stays in variables.tf (documented as a parked consumer) so a rehearsal needs no variable re-adds; tofu validate passes without pre.tf. The PRE guest is destroyed and a fresh plan on this branch reads No changes. Your infrastructure matches the configuration. The README PRE section now documents the checkout-rehearsal-destroy flow.
All three comments addressed in 0800332: 1. **tasks banner** — ADR mention removed; the banner now reads `# 4. enable Forgejo Actions + the runner`. 2. **ADR title** — now `# ADR 0001: Enable Forgejo Actions: runner on the guest, jobs in containers`. 3. **PRE resource out of main** — you were right: with the guest destroyed, every plan showed `+1 to add` for it. The rig (tofu/pre.tf, host_vars/forgejo-pre.yml, the pre inventory host line) is parked on the `forgejo-pre` branch at a593463 — the same pattern the README already documented for PRE config. `guest_pre` stays in variables.tf (documented as a parked consumer) so a rehearsal needs no variable re-adds; `tofu validate` passes without pre.tf. The PRE guest is destroyed and a fresh plan on this branch reads `No changes. Your infrastructure matches the configuration.` The README PRE section now documents the checkout-rehearsal-destroy flow.
Author
Owner

Two-axis code review — PR #5

Fixed point: main @ 4255193 (merge-base = main; diff non-empty — 13 files / 670 lines across 3 commits: a95f6e5, 3b6147e, a593463). Spec source: issue #3. Both axes ran as parallel sub-agents; findings below are kept separate, not merged or reranked. Material findings verified against the working tree.

Standards

(a) Documented-standard breaches

  1. Dangling ADR reference (soft). The ansible-vault-encrypted ansible/group_vars/forgejo/vault.yml still carries the comment # Forgejo Actions runner shared secret (ADR-0002), but the ADR is docs/adr/0001-forgejo-actions-runner-on-guest.md and every other new reference (defaults, tasks §4, runner.yaml.j2, README.md:85) says 0001. Commit 3b6147e renumbered the code but left this comment stale — invisible to grep because it sits inside the encrypted blob (verified by decrypting).
  2. Everything else respected. README's added text restates conventions 2, 3, 6; pre.tf/variables.tf/host_vars use only 10.12.0.x (conv 7); the runner secret sits in an encrypted committed file (conv 5); the runner version is pinned in defaults/main.yml (conv 4); branch+PR (conv 1). No hard violations.

(b) Baseline smells (all judgement calls)

  • Duplicated Code + Data Clumps — variables.tf 39–68: guest_pre copies guest's entire 12-field object({…}) verbatim, and pre.tf duplicates container.tf's resource body. One guests map would change the shape once.
  • Speculative Generality — ssh_keys = list(string) is declared in both guest and guest_pre, set [], and never read: both resources use local.operator_pubkey. The diff propagates a dead field.
  • Mysterious Name — the runner parent {{ forgejo_home }}/data/act-cache holds job working dirs (workdir_parent), not a cache. Rename (e.g. runner-work).
  • Primitive Obsession — forgejo_actions_unit_type: 10, a bare int for the Actions repo-unit concept, explained only by a comment.
  • Divergent Change — tasks/main.yml now carries six unrelated sections; runner provisioning could live in its own file.
  • Duplicated Code (low) — DB tasks repeat become_user: postgres/become_method: su; runner CLI tasks repeat the runuser -u … forgejo … --config prefix.

Spec

(a) Missing / partial

  • Story 18 — "runner registered with labels that match what my workflows request, so that my jobs get picked up instead of queueing forever." Unverifiable from the diff — only docker and ubuntu-latest exist, both aliased to node:22-bookworm. The consumer (wiki-sync, #2) is Out of Scope, so nothing confirms these are the labels its workflows request; any other runs-on queues forever.
  • Story 7 — "pinned in the role defaults next to the Forgejo binary pin." Pin is present and correct (13.1.0) but sits in the Actions block, not adjacent to forgejo_version. Cosmetic; convention 4 satisfied.

(b) Scope creep (behaviour not asked for)

  • runner.yaml.j2 sets job timeout: 1h — deferred story 20, not in scope.
  • tofu/pre.tf + guest_pre add a managed PRE container. Out of Scope: "PRE Guest work beyond using it as the rehearsal target it already is." Defensible as the test rig, but it is new provisioning work.
  • .gitignore .worktrees/ — unrelated to the spec.

(c) Implemented but looks wrong

  • Story 10 / Implementation Decisions — "registration token and any runner credentials are transient values used during provisioning." The shared-secret flow makes the credential explicitly long-lived (ADR: "long-lived rather than transient"). The supersession is deliberate and recorded in ADR-0001, so it's a legitimate deviation — but it contradicts the spec's secrets posture, not merely the sketched mechanism.
  • Story 12 — "a second playbook run to change nothing." The registration command is changed_when: false unconditionally (tasks/main.yml:180), so genuine server-side registration drift is invisible to Ansible. PRE-verified, but the guard is weaker than "changes nothing" implies.

Everything else maps cleanly: stories 1–6, 8–9, 11, 13–17, the container-only-label ADR claim (no host label registered), the per-repo PATCH loop narrowed to repos lacking the unit (block-level when + guarded SELECT), and PRE overrides in host_vars.


Summary — Standards: 1 soft documented-standard breach (stale ADR-0002 in the vault comment) plus 6 judgement-call smells, worst being the verbatim guest_pre/pre.tf duplication. Spec: 2 partial requirements, 3 scope-creep items, 2 implemented-but-questionable; worst is the long-lived credential contradicting the spec's stated transient-secrets posture (deliberate and ADR-recorded, acceptable if the deviation is signed off).

## Two-axis code review — PR #5 Fixed point: `main` @ `4255193` (merge-base = `main`; diff non-empty — 13 files / 670 lines across 3 commits: `a95f6e5`, `3b6147e`, `a593463`). Spec source: issue #3. Both axes ran as parallel sub-agents; findings below are kept separate, not merged or reranked. Material findings verified against the working tree. ### Standards **(a) Documented-standard breaches** 1. **Dangling ADR reference (soft).** The ansible-vault-encrypted `ansible/group_vars/forgejo/vault.yml` still carries the comment `# Forgejo Actions runner shared secret (ADR-0002)`, but the ADR is `docs/adr/0001-forgejo-actions-runner-on-guest.md` and every other new reference (defaults, tasks §4, `runner.yaml.j2`, `README.md:85`) says 0001. Commit `3b6147e` renumbered the code but left this comment stale — invisible to grep because it sits inside the encrypted blob (verified by decrypting). 2. **Everything else respected.** README's added text restates conventions 2, 3, 6; `pre.tf`/`variables.tf`/`host_vars` use only `10.12.0.x` (conv 7); the runner secret sits in an encrypted committed file (conv 5); the runner version is pinned in `defaults/main.yml` (conv 4); branch+PR (conv 1). No hard violations. **(b) Baseline smells (all judgement calls)** - **Duplicated Code + Data Clumps** — `variables.tf` 39–68: `guest_pre` copies `guest`'s entire 12-field `object({…})` verbatim, and `pre.tf` duplicates `container.tf`'s resource body. One `guests` map would change the shape once. - **Speculative Generality** — `ssh_keys = list(string)` is declared in both `guest` and `guest_pre`, set `[]`, and never read: both resources use `local.operator_pubkey`. The diff propagates a dead field. - **Mysterious Name** — the runner parent `{{ forgejo_home }}/data/act-cache` holds job working dirs (`workdir_parent`), not a cache. Rename (e.g. `runner-work`). - **Primitive Obsession** — `forgejo_actions_unit_type: 10`, a bare int for the Actions repo-unit concept, explained only by a comment. - **Divergent Change** — `tasks/main.yml` now carries six unrelated sections; runner provisioning could live in its own file. - **Duplicated Code (low)** — DB tasks repeat `become_user: postgres`/`become_method: su`; runner CLI tasks repeat the `runuser -u … forgejo … --config` prefix. ### Spec **(a) Missing / partial** - **Story 18** — *"runner registered with labels that match what my workflows request, so that my jobs get picked up instead of queueing forever."* Unverifiable from the diff — only `docker` and `ubuntu-latest` exist, both aliased to `node:22-bookworm`. The consumer (wiki-sync, #2) is Out of Scope, so nothing confirms these are the labels its workflows request; any other `runs-on` queues forever. - **Story 7** — *"pinned in the role defaults next to the Forgejo binary pin."* Pin is present and correct (`13.1.0`) but sits in the Actions block, not adjacent to `forgejo_version`. Cosmetic; convention 4 satisfied. **(b) Scope creep (behaviour not asked for)** - `runner.yaml.j2` sets job `timeout: 1h` — deferred story 20, not in scope. - `tofu/pre.tf` + `guest_pre` add a *managed* PRE container. Out of Scope: *"PRE Guest work beyond using it as the rehearsal target it already is."* Defensible as the test rig, but it is new provisioning work. - `.gitignore` `.worktrees/` — unrelated to the spec. **(c) Implemented but looks wrong** - **Story 10 / Implementation Decisions** — *"registration token and any runner credentials are transient values used during provisioning."* The shared-secret flow makes the credential explicitly long-lived (ADR: *"long-lived rather than transient"*). The supersession is deliberate and recorded in ADR-0001, so it's a legitimate deviation — but it contradicts the spec's secrets posture, not merely the sketched mechanism. - **Story 12** — *"a second playbook run to change nothing."* The registration command is `changed_when: false` unconditionally (`tasks/main.yml:180`), so genuine server-side registration drift is invisible to Ansible. PRE-verified, but the guard is weaker than "changes nothing" implies. Everything else maps cleanly: stories 1–6, 8–9, 11, 13–17, the container-only-label ADR claim (no `host` label registered), the per-repo PATCH loop narrowed to repos lacking the unit (block-level `when` + guarded SELECT), and PRE overrides in host_vars. --- **Summary** — Standards: 1 soft documented-standard breach (stale ADR-0002 in the vault comment) plus 6 judgement-call smells, worst being the verbatim `guest_pre`/`pre.tf` duplication. Spec: 2 partial requirements, 3 scope-creep items, 2 implemented-but-questionable; worst is the long-lived credential contradicting the spec's stated transient-secrets posture (deliberate and ADR-recorded, acceptable if the deviation is signed off).
Measured on the PRE guest while closing the review: registering the full
'name:backend' label strings server-side breaks job pickup. The daemon's
Declare persists only bare label NAMES into agent_labels, and task matching
(ActionRunJob.ItRunsOn) compares runs-on: against exactly those names; a
playbook re-run re-upserted the full strings behind a running daemon and new
jobs sat 'waiting' forever (FetchTask's version gate masked it — no retry
until the next version bump). Register now derives bare names from the same
defaults list; the full name->backend mapping stays in the daemon config
where it is actually consumed.

Also from the review comment (id 28):
- vault.yml: stale ADR-0002 reference inside the encrypted comment fixed
  (renumber grep missed it — inside the encrypted blob).
- act-cache -> forgejo_runner_workdir_parent (it held job workspaces, not
  the cache; the built-in cache lives under $HOME/.cache/actcache).
- runner version pin moved next to forgejo_version (story 7).
- registration changed_when now driven by a read-only action_runner
  pre-check: genuine server-side drift (row deleted in the UI) reports
  changed and is repaired; steady-state re-runs stay changed=0.
- login_db instead of the deprecated db alias (postgresql_query).

PRE-verified end to end: fresh guest converge; second run changed=0;
delete-the-runner-row drift -> registration reports changed and recreates;
new workflow push with the daemon already up -> green run in a container
(the exact scenario that was broken); labels in DB are bare names.
0800332 parked these off-main per review; 6c6dd6a must not have reintroduced
them. .scratch/ is session-local review material.
Author
Owner

All findings from the two-axis review addressed in 6c6dd6a (+ 1843511/3779e36 cleanup of an accidental sweep):

Standards

  • Vault comment said ADR-0002 — fixed inside the encrypted blob (the renumber grep had missed it because it sits in ciphertext).
  • Judgement-call smells kept deliberately: guest_pre mirrors guest's type and pre.tf mirrors container.tf (the repo's existing PRE pattern, now parked off-main anyway); forgejo_actions_unit_type stays an int — it's a forgejo DB enum value, a named constant would just alias it; the six-section tasks file follows the role's existing banner structure.

Spec

  • The label-format finding turned out to be a real bug, found live while verifying. Registering the full docker:docker://… strings server-side breaks job matching: the daemon's Declare persists only bare label names in agent_labels, and ActionRunJob.ItRunsOn compares runs-on: against exactly those names. A playbook re-run re-upserted the full strings behind a running daemon → new jobs sat waiting forever. Register now derives bare names (docker,ubuntu-latest); the name→backend mapping stays in the daemon config where it's consumed. PRE-verified with the exact regression: push with daemon up → green run in a container. ADR updated with the measured semantics.
  • Story 12 guard strengthened: changed_when for registration is now driven by a read-only action_runner pre-check — a genuinely-deleted runner row reports changed and is repaired; steady-state re-runs stay changed=0. (The earlier daemon-recreated-the-row race in my drift test was the daemon re-declaring within its 2s poll; with the daemon stopped the guard reports changed correctly.)
  • Runner pin moved next to forgejo_version (story 7 cosmetic).
  • Registration mechanism deviation (long-lived vaulted secret vs the spec's transient token): deliberate and now explicit in the ADR — Forgejo 15 replaced the token flow (deprecated in runner v13); the secret is infrastructure, not a bootstrap value. Signing off on that posture is part of this PR's review.
  • Story 18 labels: docker and ubuntu-latest are the labels this instance's workflows will use; the wiki-sync workflow (#2) will pin runs-on: docker. Any other label intentionally queues forever (no runner serves it).
  • Scope items: timeout: 1h kept (it protects the single-capacity queue; noted as deferred-scope in the template comment); PRE provisioning now parked off-main; .worktrees/ gitignore dropped with the sweep cleanup... actually it remains from the earlier commit — flagging for your call, happy to drop it.

Also fixed en route: act-cache → forgejo_runner_workdir_parent (it holds job workspaces, not the cache), and login_db instead of the deprecated db: alias.

Full PRE evidence: converge → changed=0 → row-deletion drift reports changed and repairs → clean-workflow pushes (two, incl. one after the daemon had been up through a re-register) → green runs with hello from the runner in the instance-stored logs, job in a container (Linux … 7a0fae0482a3-style hostname).

All findings from the two-axis review addressed in 6c6dd6a (+ 1843511/3779e36 cleanup of an accidental sweep): **Standards** - Vault comment said ADR-0002 — fixed inside the encrypted blob (the renumber grep had missed it because it sits in ciphertext). - Judgement-call smells kept deliberately: `guest_pre` mirrors `guest`'s type and pre.tf mirrors container.tf (the repo's existing PRE pattern, now parked off-main anyway); `forgejo_actions_unit_type` stays an int — it's a forgejo DB enum value, a named constant would just alias it; the six-section tasks file follows the role's existing banner structure. **Spec** - **The label-format finding turned out to be a real bug, found live while verifying.** Registering the full `docker:docker://…` strings server-side breaks job matching: the daemon's Declare persists only bare label *names* in `agent_labels`, and `ActionRunJob.ItRunsOn` compares `runs-on:` against exactly those names. A playbook re-run re-upserted the full strings behind a running daemon → new jobs sat `waiting` forever. Register now derives bare names (`docker,ubuntu-latest`); the name→backend mapping stays in the daemon config where it's consumed. PRE-verified with the exact regression: push with daemon up → green run in a container. ADR updated with the measured semantics. - **Story 12 guard strengthened**: `changed_when` for registration is now driven by a read-only `action_runner` pre-check — a genuinely-deleted runner row reports changed and is repaired; steady-state re-runs stay `changed=0`. (The earlier daemon-recreated-the-row race in my drift test was the daemon re-declaring within its 2s poll; with the daemon stopped the guard reports changed correctly.) - **Runner pin** moved next to `forgejo_version` (story 7 cosmetic). - **Registration mechanism deviation** (long-lived vaulted secret vs the spec's transient token): deliberate and now explicit in the ADR — Forgejo 15 replaced the token flow (deprecated in runner v13); the secret is infrastructure, not a bootstrap value. Signing off on that posture is part of this PR's review. - **Story 18 labels**: `docker` and `ubuntu-latest` are the labels this instance's workflows will use; the wiki-sync workflow (#2) will pin `runs-on: docker`. Any other label intentionally queues forever (no runner serves it). - Scope items: `timeout: 1h` kept (it protects the single-capacity queue; noted as deferred-scope in the template comment); PRE provisioning now parked off-main; `.worktrees/` gitignore dropped with the sweep cleanup... actually it remains from the earlier commit — flagging for your call, happy to drop it. Also fixed en route: `act-cache` → `forgejo_runner_workdir_parent` (it holds job workspaces, not the cache), and `login_db` instead of the deprecated `db:` alias. Full PRE evidence: converge → `changed=0` → row-deletion drift reports changed and repairs → clean-workflow pushes (two, incl. one after the daemon had been up through a re-register) → green runs with `hello from the runner` in the instance-stored logs, job in a container (`Linux … 7a0fae0482a3`-style hostname).
Author
Owner

Two-axis code review — PR #5 (re-run)

Fixed point: main @ 4255193 (merge-base = main; diff non-empty — 10 files / 496 insertions / 21 deletions across 8 commits). Spec source: issue #3. Standards sources: README.md "Conventions (mandatory reading)" + AGENTS.md. Both axes ran as parallel sub-agents; findings kept separate, not merged or reranked. Material claims verified against the working tree — one Standards finding was a false positive and is corrected inline.

Standards

(a) Documented-standard breaches

  1. Convention 7 (README:13, "no public IPs and no DDNS hostnames") vs data.forgejo.org. app.ini.j2:36 adds DEFAULT_ACTIONS_URL = https://data.forgejo.org, echoed in README:88 and in the default label image refs (defaults/main.yml:49-50, docker://data.forgejo.org/oci/node:22-bookworm). The rule's target is the operator's own public IP/DDNS; this is a third-party software mirror — so judgement on severity, not a clear hard breach. Either the convention gains a "public mirror FQDNs allowed" carve-out or the URL moves to a var.
  2. README:72-81 (PRE pattern) vs tofu/variables.tf:39-64 — verified FALSE. git ls-files --error-unmatch tofu/pre.tf → not tracked (parked, as README states). README's parked list is "tofu resource, host_vars overrides, inventory host line"; variables.tf is not in it, and the committed guest_pre description documents it is kept deliberately "so a rehearsal needs no variable re-adds." No contradiction. Its unused-on-main status is covered by the Speculative Generality smell below.

(AGENTS.md → domain.md says read GLOSSARY.md; absent, but domain.md says "proceed silently; don't flag absence." No breach. ADR correctly at docs/adr/0001-…. Version pins in one place — convention 4 satisfied.)

(b) Baseline smells (all judgement calls)

  • Duplicated Code. The read-only Postgres probe shape repeats three times — tasks/main.yml:157-164, :253-261, :326-341 — same become_user: postgres + login_db + changed_when: false + register scaffold. Extract one reusable task/include.
  • Data Clumps. variables.tf:9-22 and :39-52 carry the identical 12-field clump (vmid, hostname, ip, gateway, cores, memory, swap, datastore, disk_gb, network_interface, network_bridge, ssh_keys), duplicate default maps too — an lxc_guest type wants to be born.
  • Speculative Generality. guest_pre (variables.tf:39) has no consumer on this branch (its only caller, pre.tf, is parked).
  • Primitive Obsession. forgejo_actions_unit_type: 10 (defaults/main.yml:27) — a magic integer for the "Actions unit" concept.
  • Possible Mysterious Name. forgejo_runner_uuid derives inline from vault_forgejo_runner_secret (defaults/main.yml:71); the same credential appears as token, --secret, and a derived uuid — three names for one value.

Spec

(a) Missing / partial

  • Story 10 partial. Spec: "registered with a token generated through the Forgejo CLI using the existing runuser pattern." The diff registers a vaulted shared secret (forgejo-cli actions register --secret); a CLI-generated registration token is never produced or consumed. ADR-0001 records the deviation, but the asked mechanism is absent.
  • Story 19 ("run logs queryable from the Guest") not implemented — but it sits under "(agent follow-ups, deferred)", so expected.

(b) Scope creep

  • PRE-guest artifact left on main. tofu/variables.tf gains guest_pre (vm 142, forgejo-pre). Spec Out of Scope: "PRE Guest work beyond using it as the rehearsal target it already is." pre.tf is correctly parked off-branch, but its variable is committed with no consumer.
  • Unrelated README text. "After every tofu apply: encrypted state backup outside the repo (convention 6…)" plus a PRE-rehearsal how-to — not requested by issue #3.
  • Deferred story implemented. runner.yaml.j2:15 sets timeout: 1h, i.e. story 20, listed under "(agent follow-ups, deferred)".

(c) Implemented but looks wrong

  • Secrets posture contradicted. Spec: "registration token and any runner credentials are transient values used during provisioning." ADR-0001: the credential "is therefore long-lived rather than transient, which is the point." The deviation is recorded, but it overwrites a binding Implementation Decision rather than the sketch.
  • has_actions inferred, not read. Story 4: "I want has_actions enabled on this repo." The guard checks existence of a repo_unit.type = 10 row and only PATCHes when absent; a row that exists but is disabled would skip the PATCH, leaving CI silently off — the "half-enabled" state story 5 warns against. Low-confidence; the author says it was checked against v15.0.9's handler.

(Stories 14/15/16 adequately covered by ADR-0001: fallback recorded, RCE mitigations noted.)


Summary — Standards: 2 documented-standard findings (1 downgraded to judgement on verification, 1 false positive) plus 5 judgement-call smells; worst surviving documented-standard item is the data.forgejo.org-vs-convention-7 question, worst smell the Postgres probe scaffold duplicated three times. Spec: 7 findings (2 missing/partial, 3 scope creep, 2 implemented-but-questionable); worst is the long-lived credential overwriting the spec's stated transient-secrets posture — deliberate and ADR-recorded, so it needs sign-off rather than a code change.

## Two-axis code review — PR #5 (re-run) Fixed point: `main` @ `4255193` (merge-base = `main`; diff non-empty — 10 files / 496 insertions / 21 deletions across 8 commits). Spec source: issue #3. Standards sources: `README.md` "Conventions (mandatory reading)" + `AGENTS.md`. Both axes ran as parallel sub-agents; findings kept separate, not merged or reranked. Material claims verified against the working tree — one Standards finding was a false positive and is corrected inline. ### Standards **(a) Documented-standard breaches** 1. **Convention 7 (README:13, "no public IPs and no DDNS hostnames") vs `data.forgejo.org`.** `app.ini.j2:36` adds `DEFAULT_ACTIONS_URL = https://data.forgejo.org`, echoed in `README:88` and in the default label image refs (`defaults/main.yml:49-50`, `docker://data.forgejo.org/oci/node:22-bookworm`). The rule's target is the operator's own public IP/DDNS; this is a third-party software mirror — so judgement on severity, not a clear hard breach. Either the convention gains a "public mirror FQDNs allowed" carve-out or the URL moves to a var. 2. ~~README:72-81 (PRE pattern) vs `tofu/variables.tf:39-64`~~ — **verified FALSE.** `git ls-files --error-unmatch tofu/pre.tf` → not tracked (parked, as README states). README's parked list is "tofu resource, host_vars overrides, inventory host line"; `variables.tf` is not in it, and the committed `guest_pre` description documents it is kept deliberately "so a rehearsal needs no variable re-adds." No contradiction. Its unused-on-main status is covered by the Speculative Generality smell below. *(AGENTS.md → domain.md says read `GLOSSARY.md`; absent, but domain.md says "proceed silently; don't flag absence." No breach. ADR correctly at `docs/adr/0001-…`. Version pins in one place — convention 4 satisfied.)* **(b) Baseline smells (all judgement calls)** - **Duplicated Code.** The read-only Postgres probe shape repeats three times — `tasks/main.yml:157-164`, `:253-261`, `:326-341` — same `become_user: postgres` + `login_db` + `changed_when: false` + `register` scaffold. Extract one reusable task/include. - **Data Clumps.** `variables.tf:9-22` and `:39-52` carry the identical 12-field clump (`vmid, hostname, ip, gateway, cores, memory, swap, datastore, disk_gb, network_interface, network_bridge, ssh_keys`), duplicate default maps too — an `lxc_guest` type wants to be born. - **Speculative Generality.** `guest_pre` (`variables.tf:39`) has no consumer on this branch (its only caller, `pre.tf`, is parked). - **Primitive Obsession.** `forgejo_actions_unit_type: 10` (`defaults/main.yml:27`) — a magic integer for the "Actions unit" concept. - **Possible Mysterious Name.** `forgejo_runner_uuid` derives inline from `vault_forgejo_runner_secret` (`defaults/main.yml:71`); the same credential appears as `token`, `--secret`, and a derived `uuid` — three names for one value. ### Spec **(a) Missing / partial** - **Story 10 partial.** Spec: *"registered with a token generated through the Forgejo CLI using the existing runuser pattern."* The diff registers a vaulted **shared secret** (`forgejo-cli actions register --secret`); a CLI-generated registration token is never produced or consumed. ADR-0001 records the deviation, but the asked mechanism is absent. - **Story 19** (*"run logs queryable from the Guest"*) not implemented — but it sits under *"(agent follow-ups, deferred)"*, so expected. **(b) Scope creep** - **PRE-guest artifact left on main.** `tofu/variables.tf` gains `guest_pre` (vm 142, `forgejo-pre`). Spec Out of Scope: *"PRE Guest work beyond using it as the rehearsal target it already is."* `pre.tf` is correctly parked off-branch, but its variable is committed with no consumer. - **Unrelated README text.** *"After every tofu apply: encrypted state backup outside the repo (convention 6…)"* plus a PRE-rehearsal how-to — not requested by issue #3. - **Deferred story implemented.** `runner.yaml.j2:15` sets `timeout: 1h`, i.e. story 20, listed under *"(agent follow-ups, deferred)"*. **(c) Implemented but looks wrong** - **Secrets posture contradicted.** Spec: *"registration token and any runner credentials are transient values used during provisioning."* ADR-0001: the credential *"is therefore long-lived rather than transient, which is the point."* The deviation is recorded, but it overwrites a binding Implementation Decision rather than the sketch. - **`has_actions` inferred, not read.** Story 4: *"I want `has_actions` enabled on this repo."* The guard checks existence of a `repo_unit.type = 10` row and only PATCHes when absent; a row that exists but is disabled would skip the PATCH, leaving CI silently off — the *"half-enabled"* state story 5 warns against. Low-confidence; the author says it was checked against v15.0.9's handler. *(Stories 14/15/16 adequately covered by ADR-0001: fallback recorded, RCE mitigations noted.)* --- **Summary** — Standards: 2 documented-standard findings (1 downgraded to judgement on verification, 1 false positive) plus 5 judgement-call smells; worst surviving documented-standard item is the `data.forgejo.org`-vs-convention-7 question, worst smell the Postgres probe scaffold duplicated three times. Spec: 7 findings (2 missing/partial, 3 scope creep, 2 implemented-but-questionable); worst is the long-lived credential overwriting the spec's stated transient-secrets posture — deliberate and ADR-recorded, so it needs sign-off rather than a code change.
Standards + Spec findings from the re-run two-axis review, verified against
forgejo v15.0.9 source and the live PRE guest:

- Convention 7 vs data.forgejo.org: README gains the carve-out (third-party
  mirrors are not the operator's own addresses) and the host moves to
  forgejo_actions_mirror, one value for both app.ini (scheme added) and the
  label image refs (bare host). Caught while verifying: my first cut reused
  one URL in both places, which produced docker://host/... -> docker://https://host/...
  in the label list and broke Declare on the live guest (daemon crash-looped
  "unauthenticated: unregistered runner"). Fixed and re-verified green.
- Smell: the three read-only Postgres probes now share one YAML anchor.
  module_defaults was tried first and rejected live -- postgresql_query
  refuses become/become_user/become_method as module params.
- Spec: has_actions is inferred from row *existence*, not a disabled flag --
  v15.0.9 UnitEnabled is row-existence (models/repo/repo.go:399) and the API
  derives has_actions from GetUnit()==nil (services/convert/repository.go:137),
  with disable deleting the row (services/repository/setting.go:45). Comment
  now records that, plus that the role is authoritative (deliberate UI disable
  is repaired; opt out via forgejo_actions_repos).
- Spec: runner.yaml backstop comment points at forgejo's real default
  (ENDLESS_TASK_TIMEOUT, 3h -- modules/setting/actions.go:116).
- Pre-existing README breakage fixed: the PRE rehearsal how-to's checkout
  list omitted the pre/hosts line and the `git checkout forgejo-pre --`
  command spanned lines mid-token. Rewritten as a fenced block; verified the
  branch host line differs from main's and the cleanup step is correct.

Verified: clean converge on PRE (changed=0), runner active + heartbeating,
labels registered bare (docker, ubuntu-latest), pushed workflow run 5 green
(after the mirror-var fix, so it exercised the new templates), tofu plan
No changes.
Author
Owner

All findings from the re-run review addressed in forgejo-actions @ e5b1f5f (pushed; PR now e16531d..e5b1f5f). Every claims-bearing fix was verified against forgejo v15.0.9 source and re-run live on the PRE guest.

Standards

  • Convention 7 vs data.forgejo.org. Took the carve-out route and moved the value: README convention 7 now says third-party software mirrors are allowed (the rule targets the operator's own IP/DDNS — that's the actual wording), and the host lives in forgejo_actions_mirror (role defaults). app.ini takes it with the scheme added; the label image refs take the bare host. Verified on PRE: app.ini renders DEFAULT_ACTIONS_URL = https://data.forgejo.org, the labels render docker:docker://data.forgejo.org/oci/node:22-bookworm.
  • Probe scaffold duplicated 3×. Now one YAML anchor (&forgejo_pg_probe) on the three read-only postgresql_query probes. Two things worth recording: (a) module_defaults — the obvious tool — does not work here; community.postgresql.postgresql_query rejects become/become_user/become_method as module params (they're task keywords). Confirmed live, both in isolation and via the actual playbook run. (b) I did not use an include: it either loses register or needs a fragile set_fact indirection because include-level until/retries is rejected by TaskInclude. The anchor is honest about what it is.
  • forgejo_actions_unit_type comment now cites v15.0.9 models/unit/unit.go:35 and the handler lines, and states it's a DB enum value, not a tunable.

Spec

  • has_actions inferred, not read — you were right, and it's worse than "low confidence": it's actually correct, and my comment was the misleading part. v15.0.9 stores an enabled unit as a repo_unit row and a disabled one as a deleted row — services/repository/setting.go:45 does DELETE ... WHERE type IN (deleteUnitTypes), and both readers are "does the row exist": UnitEnabled (models/repo/repo.go:399-409) and the API's has_actions (services/convert/repository.go:137-140, GetUnit(...TypeActions) == nil). So existence is the flag; there is no "row present but blanked" state to miss. Folding config into the test would flag the working row (empty config, default_permissions 0 — queried on PRE) as broken and PATCH every run. Rewrote the §6 comment to record all of that, plus the explicit posture: the role is authoritative, a UI disable is repaired on the next run (that's story 5), and to keep a repo's Actions off on purpose you drop it from forgejo_actions_repos.
  • data.forgejo.org scope — folded into the convention-7 item above.
  • guest_pre / README. Kept guest_pre (bare variable, no consumer, creates nothing — parked pre.tf is off-main as README says), and made the README's parked list explicit about it. The false-positive half I left alone.
  • Unrelated README text. The convention-6 state-backup pointer moved next to the NPM flow's own "after every apply" step (line 41) and the trailing copy is gone; the PRE how-to is what makes the parked rig usable, so it stays but now actually works (below).
  • timeout: 1h (deferred story 20). Kept, deliberately — an anti-wedge guard on a capacity-1 queue. Comment now names the real backstop: forgejo's own ENDLESS_TASK_TIMEOUT default, 3h (modules/setting/actions.go:116).
  • Long-lived credential vs transient posture. Already ADR-recorded; this is the sign-off item, not a code change — flagging it as the open call on this PR, same as last round.

Caught while verifying (not raised in the review)

  • My own first cut of the mirror variable produced docker://host/... → docker://https://host/... in the label list. The daemon crash-looped unauthenticated: unregistered runner on the live guest. Fixed to a bare host (the docker://registry/image form takes no scheme) and re-verified green — so the pushed run below did exercise the new templates.
  • The README PRE rehearsal how-to was broken three ways: its git checkout forgejo-pre -- command spanned lines mid-token, its file list omitted the one file that actually differs (inventories/pre/hosts), and the branch's inventory line is forgejo-pre, not main's forgejo — so a rehearsal run against main's inventory silently loses the host_vars override and points the runner at the prod URL. That last one is exactly the failure mode called out in the earlier review; the how-to now reproduces it if followed with main's tree. Rewritten as a fenced block with the branch copy used as-is, and I verified the branch host line and the cleanup step.
  • .worktrees/ gitignore: dropped in e16531d (earlier commit), as flagged in #34.

Evidence (live, on PRE LXC 142, this round)

  • ok=26 changed=4 failed=0 on the fix run, then changed=0 on re-run (runner daemon active, last_online heartbeating ~2s ago, agent_labels = ["docker","ubuntu-latest"] — bare names, so job matching holds).
  • Pushed a trivial workflow to pit/smoke after the mirror fix: run 5 status=success (1), job landed on runner id 4, action_run_job.runs_on = ["docker"]. Its log is in actions_log/pit/smoke/02/2.log.zst with hello from the runner; the runner's own journal shows task 3 repo is pit/smoke https://data.forgejo.org http://10.12.0.142:3080/.
  • Vault ADR-0002 comment still reads ADR-0001 (decrypted to check).
  • tofu plan → No changes; prod guest 141 untouched.
All findings from the re-run review addressed in `forgejo-actions` @ `e5b1f5f` (pushed; PR now e16531d..e5b1f5f). Every claims-bearing fix was verified against forgejo v15.0.9 source and re-run live on the PRE guest. **Standards** - **Convention 7 vs `data.forgejo.org`.** Took the carve-out route *and* moved the value: README convention 7 now says third-party software mirrors are allowed (the rule targets the operator's own IP/DDNS — that's the actual wording), and the host lives in `forgejo_actions_mirror` (role defaults). app.ini takes it with the scheme added; the label image refs take the bare host. Verified on PRE: app.ini renders `DEFAULT_ACTIONS_URL = https://data.forgejo.org`, the labels render `docker:docker://data.forgejo.org/oci/node:22-bookworm`. - **Probe scaffold duplicated 3×.** Now one YAML anchor (`&forgejo_pg_probe`) on the three read-only `postgresql_query` probes. Two things worth recording: (a) `module_defaults` — the obvious tool — **does not work here**; `community.postgresql.postgresql_query` rejects `become`/`become_user`/`become_method` as module params (they're task keywords). Confirmed live, both in isolation and via the actual playbook run. (b) I did not use an include: it either loses `register` or needs a fragile `set_fact` indirection because include-level `until`/`retries` is rejected by TaskInclude. The anchor is honest about what it is. - `forgejo_actions_unit_type` comment now cites v15.0.9 `models/unit/unit.go:35` and the handler lines, and states it's a DB enum value, not a tunable. **Spec** - **`has_actions` inferred, not read — you were right, and it's worse than "low confidence": it's actually *correct*, and my comment was the misleading part.** v15.0.9 stores an enabled unit as a `repo_unit` row and a disabled one as a *deleted* row — `services/repository/setting.go:45` does `DELETE ... WHERE type IN (deleteUnitTypes)`, and both readers are "does the row exist": `UnitEnabled` (`models/repo/repo.go:399-409`) and the API's `has_actions` (`services/convert/repository.go:137-140`, `GetUnit(...TypeActions) == nil`). So existence *is* the flag; there is no "row present but blanked" state to miss. Folding `config` into the test would flag the working row (empty config, `default_permissions 0` — queried on PRE) as broken and PATCH every run. Rewrote the §6 comment to record all of that, plus the explicit posture: the role is authoritative, a UI disable is repaired on the next run (that's story 5), and to keep a repo's Actions off on purpose you drop it from `forgejo_actions_repos`. - **`data.forgejo.org` scope** — folded into the convention-7 item above. - **`guest_pre` / README.** Kept `guest_pre` (bare variable, no consumer, creates nothing — parked `pre.tf` is off-main as README says), and made the README's parked list explicit about it. The false-positive half I left alone. - **Unrelated README text.** The convention-6 state-backup pointer moved next to the NPM flow's own "after every apply" step (line 41) and the trailing copy is gone; the PRE how-to *is* what makes the parked rig usable, so it stays but now actually works (below). - **`timeout: 1h` (deferred story 20).** Kept, deliberately — an anti-wedge guard on a capacity-1 queue. Comment now names the real backstop: forgejo's own `ENDLESS_TASK_TIMEOUT` default, 3h (`modules/setting/actions.go:116`). - **Long-lived credential vs transient posture.** Already ADR-recorded; this is the sign-off item, not a code change — flagging it as the open call on this PR, same as last round. **Caught while verifying (not raised in the review)** - My own first cut of the mirror variable produced `docker://host/...` → `docker://https://host/...` in the label list. The daemon crash-looped `unauthenticated: unregistered runner` on the live guest. Fixed to a bare host (the `docker://registry/image` form takes no scheme) and re-verified green — so the pushed run below *did* exercise the new templates. - The README PRE rehearsal how-to was broken three ways: its `git checkout forgejo-pre --` command spanned lines mid-token, its file list omitted the one file that actually differs (`inventories/pre/hosts`), and the branch's inventory line is `forgejo-pre`, not main's `forgejo` — so a rehearsal run against main's inventory silently loses the `host_vars` override and points the runner at the prod URL. That last one is exactly the failure mode called out in the earlier review; the how-to now reproduces it if followed with main's tree. Rewritten as a fenced block with the branch copy used as-is, and I verified the branch host line and the cleanup step. - `.worktrees/` gitignore: dropped in `e16531d` (earlier commit), as flagged in #34. **Evidence (live, on PRE LXC 142, this round)** - `ok=26 changed=4 failed=0` on the fix run, then `changed=0` on re-run (runner daemon active, `last_online` heartbeating ~2s ago, `agent_labels = ["docker","ubuntu-latest"]` — bare names, so job matching holds). - Pushed a trivial workflow to `pit/smoke` after the mirror fix: run 5 `status=success` (1), job landed on runner id 4, `action_run_job.runs_on = ["docker"]`. Its log is in `actions_log/pit/smoke/02/2.log.zst` with `hello from the runner`; the runner's own journal shows `task 3 repo is pit/smoke https://data.forgejo.org http://10.12.0.142:3080/`. - Vault `ADR-0002` comment still reads `ADR-0001` (decrypted to check). - `tofu plan` → `No changes`; prod guest 141 untouched.
Author
Owner

Must-fix from the re-run review — PR #5 @ e5b1f5f

Only one item from the re-run review is a required change; the rest are judgement calls or sign-offs (listed at the bottom).

HARD — code.forgejo.org hardcoded: ansible/roles/forgejo/tasks/main.yml:149,154

This PR introduces convention 7's new single-home clause and then breaks it in the same diff:

  • README.md:13 (added by this PR) now reads: "The mirror host itself lives in forgejo_actions_mirror (role defaults), not scattered across templates."
  • The runner download hardcodes a second mirror host:
    • :149 — url: "https://code.forgejo.org/forgejo/runner/releases/download/v{{ forgejo_runner_version }}/forgejo-runner-…-linux-amd64"
    • :154 — checksum: "sha256:https://code.forgejo.org/forgejo/runner/releases/download/v{{ forgejo_runner_version }}/…-linux-amd64.sha256"
  • git grep code.forgejo.org main → no hits, so both the rule and the violation are new on this branch.

It also falsifies the README's own claim at :105-108 — "To move off Forgejo's mirror, change that one value." That holds for the label image refs (which use {{ forgejo_actions_mirror }}) but not for the runner fetch.

Not runtime-breaking — a consistency breach. Fix: hoist the runner-download host into forgejo_actions_mirror (or a sibling role-default var, since the runner fetch has no scheme/scheme-mixed usage) and use it at both lines.

Open call — not a code fix, needs sign-off

  • Secrets posture. Spec Implementation Decisions: "registration token and any runner credentials are transient values used during provisioning." ADR-0001 deliberately makes the credential long-lived. Recorded deviation — needs your sign-off; no edit required unless you disagree with the posture.

Judgement calls kept deliberately (no action needed)

guest_pre unused-on-main var, forgejo_actions_unit_type: 10 primitive, forgejo_runner_uuid name, timeout: 1h (deferred story 20), and the residual changed_when: false repeat on the Postgres probes — all reviewed, all with stated reasoning.

## Must-fix from the re-run review — PR #5 @ `e5b1f5f` Only one item from the re-run review is a required change; the rest are judgement calls or sign-offs (listed at the bottom). ### HARD — `code.forgejo.org` hardcoded: `ansible/roles/forgejo/tasks/main.yml:149,154` This PR introduces convention 7's new single-home clause and then breaks it in the same diff: - `README.md:13` (added by this PR) now reads: *"The mirror host itself lives in `forgejo_actions_mirror` (role defaults), not scattered across templates."* - The runner download hardcodes a **second** mirror host: - `:149` — `url: "https://code.forgejo.org/forgejo/runner/releases/download/v{{ forgejo_runner_version }}/forgejo-runner-…-linux-amd64"` - `:154` — `checksum: "sha256:https://code.forgejo.org/forgejo/runner/releases/download/v{{ forgejo_runner_version }}/…-linux-amd64.sha256"` - `git grep code.forgejo.org main` → no hits, so both the rule and the violation are new on this branch. It also falsifies the README's own claim at `:105-108` — *"To move off Forgejo's mirror, change that one value."* That holds for the label image refs (which use `{{ forgejo_actions_mirror }}`) but not for the runner fetch. Not runtime-breaking — a consistency breach. Fix: hoist the runner-download host into `forgejo_actions_mirror` (or a sibling role-default var, since the runner fetch has no scheme/scheme-mixed usage) and use it at both lines. ### Open call — not a code fix, needs sign-off - **Secrets posture.** Spec Implementation Decisions: *"registration token and any runner credentials are transient values used during provisioning."* ADR-0001 deliberately makes the credential long-lived. Recorded deviation — needs your sign-off; no edit required unless you disagree with the posture. ### Judgement calls kept deliberately (no action needed) `guest_pre` unused-on-main var, `forgejo_actions_unit_type: 10` primitive, `forgejo_runner_uuid` name, `timeout: 1h` (deferred story 20), and the residual `changed_when: false` repeat on the Postgres probes — all reviewed, all with stated reasoning.
tasks §4 hardcoded code.forgejo.org for the runner binary and its
checksum, while README:13 claims the mirror host lives in role defaults
and isn't scattered across templates. Move it to its own default,
forgejo_runner_download_host.

Deliberately a sibling var rather than a second use of
forgejo_actions_mirror: the mirror serves `uses:` and the job images,
while runner releases live on Forgejo's source forge and are not served
by the mirror (data.forgejo.org 404s that path), so one value covering
both would be false. README:105-110 and ADR-0001's carve-out entry now
say so instead of overclaiming.
Author
Owner

Fix in 6753880 (pushed; PR now e5b1f5f..6753880).

The HARD item is closed as suggested — with one correction to the suggestion. I did not reuse forgejo_actions_mirror; I hoisted the host into a sibling role default:

  • defaults/main.yml — new forgejo_runner_download_host: "code.forgejo.org".
  • tasks/main.yml:149,154 — url and the sha256:… checksum both take https://{{ forgejo_runner_download_host }}/….

Why a sibling var rather than the mirror var: they are not the same host, and can never be made one. data.forgejo.org is the Actions package mirror (it serves uses: and the job images); the runner release lives on Forgejo's source forge, and the mirror does not serve it — data.forgejo.org/forgejo/runner/releases/download/v13.1.0/forgejo-runner-13.1.0-linux-amd64.sha256 returns 404 (checked live, just now). Pointing both at one value would have been a one-liner that silently breaks the runner install. Two hosts, two vars, each a one-line change — which is what the README claim needed to become true for both reads.

Doc truth fixed in the same commit:

  • README.md:105-110 — the "change that one value" sentence now covers both fetches: the mirror var for uses:/job images, forgejo_runner_download_host for the runner release.
  • ADR-0001 carve-out entry now names both hosts and records that they are deliberately two variables (with the 404 as the reason).

Verified:

  • ansible-playbook -i inventories/prod/hosts site.yml --syntax-check → clean (only the pre-existing group/host same-name warning).
  • Exercised the actual task, not just the template: a scratch play loading the role defaults fetched the checksum through the templated URL and then the binary with checksum: sha256:https://{{ … }} validation — resolved to …/v13.1.0/forgejo-runner-13.1.0-linux-amd64[.sha256], download green, sha256 29dae21e…4bc376 matched. Scratch artifacts removed.
  • git grep code.forgejo.org in ansible/ now hits only the new default; no hardcoded host left in tasks.

Still open, unchanged: the secrets-posture sign-off (spec transient vs ADR long-lived) — no code change, your call. Everything else from comment 41 was the judgement-call list, left as reviewed.

Fix in `6753880` (pushed; PR now `e5b1f5f..6753880`). **The HARD item is closed as suggested — with one correction to the suggestion.** I did *not* reuse `forgejo_actions_mirror`; I hoisted the host into a sibling role default: - `defaults/main.yml` — new `forgejo_runner_download_host: "code.forgejo.org"`. - `tasks/main.yml:149,154` — `url` and the `sha256:…` checksum both take `https://{{ forgejo_runner_download_host }}/…`. Why a sibling var rather than the mirror var: they are not the same host, and can never be made one. `data.forgejo.org` is the Actions package mirror (it serves `uses:` and the job images); the runner *release* lives on Forgejo's source forge, and the mirror does not serve it — `data.forgejo.org/forgejo/runner/releases/download/v13.1.0/forgejo-runner-13.1.0-linux-amd64.sha256` returns **404** (checked live, just now). Pointing both at one value would have been a one-liner that silently breaks the runner install. Two hosts, two vars, each a one-line change — which is what the README claim needed to become true for both reads. **Doc truth fixed in the same commit:** - `README.md:105-110` — the "change that one value" sentence now covers both fetches: the mirror var for `uses:`/job images, `forgejo_runner_download_host` for the runner release. - `ADR-0001` carve-out entry now names both hosts and records that they are deliberately two variables (with the 404 as the reason). **Verified:** - `ansible-playbook -i inventories/prod/hosts site.yml --syntax-check` → clean (only the pre-existing group/host same-name warning). - Exercised the actual task, not just the template: a scratch play loading the role defaults fetched the checksum *through* the templated URL and then the binary with `checksum: sha256:https://{{ … }}` validation — resolved to `…/v13.1.0/forgejo-runner-13.1.0-linux-amd64[.sha256]`, download green, sha256 `29dae21e…4bc376` matched. Scratch artifacts removed. - `git grep code.forgejo.org` in `ansible/` now hits only the new default; no hardcoded host left in tasks. **Still open, unchanged:** the secrets-posture sign-off (spec transient vs ADR long-lived) — no code change, your call. Everything else from comment 41 was the judgement-call list, left as reviewed.
pit merged commit 9857b3ad69 into main 2026-10-06 06:45:40 +00:00
pit deleted branch forgejo-actions 2026-10-06 06:45:40 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
olympus/infra-forge!5
No description provided.