HOMELAB-1735: pull-only Harbor robot seed from OpenBao + stop CI workflows stripping the shared docker config #521

Merged
aaron merged 2 commits from plane/HOMELAB-1735-harbor-robot into live 2026-09-29 07:49:03 +00:00
Owner

What

Two related fixes for the Forgejo runner fleet's Harbor credential (HOMELAB-1735):

  1. core/manifests/external-secrets/es-forgejo-runner.yaml — the runner-harbor-robot seed (mounted into the runner daemon's /home/runner/.docker and dind's /root/.docker) now reads a dedicated pull-only robot from OpenBao kv/app-credentials instead of sharing sendrast's own workload robot (kv/sendrast/harbor).
  2. .forgejo/workflows/runner-image-build.yaml + .forgejo/workflows/docs-build.yaml — log in to a throwaway DOCKER_CONFIG and drop docker logout; both now use the scoped push robot repo secrets HARBOR_PUSH_USERNAME / HARBOR_PUSH_PASSWORD.
  3. Comment-only update in statefulset.yaml documenting the robot's scope.

Why (root cause, re-verified)

The k8s/infra runner labels are :host mode, so workflow steps run inside the runner container as runner with HOME=/home/runner and DOCKER_CONFIG unset. docker logout therefore rewrites the pod-shared emptyDir seeded once per pod from the runner-harbor-robot secret and also used by the runner daemon for job-image pre-pulls (which happen before any workflow step can docker login).

Re-verified on 2026-09-29:

Evidence Value
kubectl get secret runner-harbor-robot -n forgejo-runner (.dockerconfigjson decoded) 202 bytes (harbor entry present)
stat -c '%s %y' /home/runner/.docker/config.json on runner-0/4/7 16 bytes ({"auths":{}}), mtimes 2026-09-25 05:27, 2026-09-29 04:03:37, 2026-09-29 04:07:57
Pod start times all 2026-09-22T04:2x–04:5x

The mtimes match CI pushes, not pod starts — i.e. login+logout pairs, not a seeding failure. The seed secret itself is intact.

Scope decision (evidence-based)

Harbor robots created (admin API, secrets never printed):

Robot Level Permission Used by
robot$forgejo-runner-pull system project sendrast → repository:pull the ESO daemon/dind seed
robot$forgejo-runner-push system projects platform and lab-director → repository:pull + repository:push infra-core workflows

Why exactly that:

  • Private projects: sendrast (5 repos) and paperclip (0 repos). Every other project (docker-cache, platform, lab-director, k8s-cache, ghcr-cache, apps, library) is public → anonymous pull.
  • The only private image the runners actually pre-pull is harbor.aaron.reynoza.org/sendrast/e2e-runner:*; harbor.aaron.reynoza.org/docker-cache/catthehacker/ubuntu:act-22.04 is public. So the daemon needs sendrast pull only.
  • Push: the push destinations of this repo's publishes are platform/forgejo-runner (runner-image-build), platform/docs (docs-build) and lab-director/pi (pi-image-build) → the robot covers platform + lab-director only. It deliberately does not cover sendrast/paperclip/anything else, and has no project-create/delete/list/tag permissions (unlike the all-projects robot$ci-push it is meant to retire).
  • The new pull robot also decouples runner pods from sendrast's workload credential: previously every :host job could read the sendrast namespace's own registry token out of the shared emptyDir.

Capability probes against the registry (401/403 = denied, 200/202 = allowed):

pull-robot  GET  sendrast/e2e-runner manifest      -> 200   (intended)
pull-robot  POST sendrast blob-upload              -> 401   (no push)
pull-robot  POST platform blob-upload              -> 401   (no push)
pull-robot  GET  paperclip repos API               -> 403   (out of scope)
pull-robot  GET  lab-director/pi manifest          -> 200   (public project, anonymous)
push-robot  POST platform/test blob-upload         -> 202   (push works)
push-robot  POST platform/forgejo-runner upload     -> 202   (push works)
push-robot  POST platform/docs blob-upload         -> 202   (push works)
push-robot  POST lab-director/pi blob-upload        -> 202   (push works)
push-robot  GET  platform/docs manifest            -> 200   (pull works)
push-robot  POST sendrast/e2e-runner blob-upload   -> 401   (out of scope)
push-robot  GET  sendrast/e2e-runner manifest      -> 401   (out of scope)
push-robot  GET  paperclip repos API               -> 403   (out of scope)

Out-of-band state already applied (not in Git)

  • Robot accounts created in Harbor; secrets stored in OpenBao kv/app-credentials:
    forgejo_runner_harbor_pull_username / _pull_token / _push_username / _push_token (read-back hash-verified; values never printed).
  • Repo secrets HARBOR_PUSH_USERNAME / HARBOR_PUSH_PASSWORD created on Homelab/infra-core (base64 via API). They are inert until this PR merges; rollback = delete those two repo secrets (plus the robots in Harbor).

Rollout caution (read before merging)

This changes a secret that all 8 runner pods consume. ArgoCD will sync the ExternalSecret after merge, but pods keep the config they were seeded with at start — existing pods stay on the old credential until they cycle. Until then their {"auths":{}}-stripped configs remain stripped, so private pre-pulls stay broken on those pods regardless of this PR.

Operator sequence (safe; no restart performed by the agent):

  1. Merge this PR; let ArgoCD sync external-secrets-config and confirm kubectl get externalsecret -n forgejo-runner runner-harbor-robot -o jsonpath='{.status.conditions[0]}' → SecretSynced.
  2. Confirm the synced Secret changed (kubectl get secret runner-harbor-robot -n forgejo-runner -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r '.auths|keys[]' → harbor.aaron.reynoza.org; the username inside is now robot$forgejo-runner-pull).
  3. Cycle the runner pods at a maintenance moment — a deliberate fleet action (8 pods, kubectl rollout restart statefulset/forgejo-runner -n forgejo-runner). Pods re-run the docker-config initContainer and re-seed from the new secret. Registration state lives on per-pod PVCs, so a cycle is safe.
  4. Verify per ticket: on a restarted pod, stat -c %y /home/runner/.docker/config.json mtime must not move after a push run of either fixed workflow (that is the proof the logout footgun is gone).

Verification done here (offline)

  • yamllint -d relaxed clean of new errors; both workflow YAMLs parse; the modified run steps pass bash -n.
  • kubectl apply --dry-run=server -f core/manifests/external-secrets/es-forgejo-runner.yaml → schema valid, no write.
  • The ESO dockerconfigjson template renders valid JSON with the new values (auth round-trips).
  • OpenBao read-back hash-compare for all four new keys.
  • Live workflow runs / mtime proof / pre-pull success cannot be verified before merge + pod cycle — see rollout steps.

Not done (deliberately, with rationale)

  • pi-image-build.yaml untouched (its DOCKER_CONFIG pattern is already correct and the brief scoped it out) — it still uses HARBOR_*, so retiring that admin-grade repo secret needs a follow-up that migrates it to HARBOR_PUSH_* (the robot already covers lab-director/pi so that follow-up is a one-line secret rename). Its header comment still describes HARBOR_* as a shared admin, which will be stale after the swap.
  • The same class of bug exists in other repos on the same runner labels (full API sweep of all 30 accessible repos, default + live branches):
    • Sendrast/api-sendrast — ci-app.yml:133, release.yml:100: unisolated docker login + docker logout (strips the entry).
    • Sendrast/sendrast-web — e2e-image.yml:93: unisolated docker login + docker logout (strips the entry).
    • Homelab/lab-director — build.yaml + build-sandbox.yaml (4 logins): unisolated docker login, no logout — it does not delete the entry but overwrites the pod-shared one with a lab-director push credential, which then breaks the daemon's sendrast pre-pull.
      Separate PR per repo; listed on the ticket rather than drive-by changed here.
  • The "guard" scope item (read-only mount / checksum alert) is not implemented: a read-only /home/runner/.docker would break every remaining :host workflow that still does a plain docker login, and a repo-local lint cannot protect other repos. Proposal recorded on the ticket.
## What Two related fixes for the Forgejo runner fleet's Harbor credential (HOMELAB-1735): 1. **`core/manifests/external-secrets/es-forgejo-runner.yaml`** — the `runner-harbor-robot` seed (mounted into the runner daemon's `/home/runner/.docker` and dind's `/root/.docker`) now reads a **dedicated pull-only robot** from OpenBao `kv/app-credentials` instead of sharing sendrast's own workload robot (`kv/sendrast/harbor`). 2. **`.forgejo/workflows/runner-image-build.yaml` + `.forgejo/workflows/docs-build.yaml`** — log in to a throwaway `DOCKER_CONFIG` and **drop `docker logout`**; both now use the scoped push robot repo secrets `HARBOR_PUSH_USERNAME` / `HARBOR_PUSH_PASSWORD`. 3. Comment-only update in `statefulset.yaml` documenting the robot's scope. ## Why (root cause, re-verified) The `k8s`/`infra` runner labels are `:host` mode, so workflow steps run **inside the runner container** as `runner` with `HOME=/home/runner` and `DOCKER_CONFIG` unset. `docker logout` therefore rewrites the **pod-shared** emptyDir seeded once per pod from the `runner-harbor-robot` secret and also used by the runner daemon for **job-image pre-pulls** (which happen before any workflow step can `docker login`). Re-verified on 2026-09-29: | Evidence | Value | |---|---| | `kubectl get secret runner-harbor-robot -n forgejo-runner` (`.dockerconfigjson` decoded) | 202 bytes (harbor entry present) | | `stat -c '%s %y' /home/runner/.docker/config.json` on runner-0/4/7 | 16 bytes (`{"auths":{}}`), mtimes 2026-09-25 05:27, 2026-09-29 04:03:37, 2026-09-29 04:07:57 | | Pod start times | all 2026-09-22T04:2x–04:5x | The mtimes match CI pushes, not pod starts — i.e. login+logout pairs, not a seeding failure. The seed secret itself is intact. ## Scope decision (evidence-based) Harbor robots created (admin API, secrets never printed): | Robot | Level | Permission | Used by | |---|---|---|---| | `robot$forgejo-runner-pull` | system | project `sendrast` → `repository:pull` | the ESO daemon/dind seed | | `robot$forgejo-runner-push` | system | projects `platform` **and** `lab-director` → `repository:pull` + `repository:push` | infra-core workflows | Why exactly that: - **Private projects:** `sendrast` (5 repos) and `paperclip` (**0 repos**). Every other project (`docker-cache`, `platform`, `lab-director`, `k8s-cache`, `ghcr-cache`, `apps`, `library`) is public → anonymous pull. - The only private image the runners actually pre-pull is `harbor.aaron.reynoza.org/sendrast/e2e-runner:*`; `harbor.aaron.reynoza.org/docker-cache/catthehacker/ubuntu:act-22.04` is public. So the daemon needs **`sendrast` pull only**. - Push: the push destinations of this repo's publishes are `platform/forgejo-runner` (runner-image-build), `platform/docs` (docs-build) and `lab-director/pi` (pi-image-build) → the robot covers **`platform` + `lab-director` only**. It deliberately does **not** cover `sendrast`/`paperclip`/anything else, and has no project-create/delete/list/tag permissions (unlike the all-projects `robot$ci-push` it is meant to retire). - The new pull robot also **decouples** runner pods from sendrast's workload credential: previously every `:host` job could read the sendrast namespace's own registry token out of the shared emptyDir. Capability probes against the registry (401/403 = denied, 200/202 = allowed): ``` pull-robot GET sendrast/e2e-runner manifest -> 200 (intended) pull-robot POST sendrast blob-upload -> 401 (no push) pull-robot POST platform blob-upload -> 401 (no push) pull-robot GET paperclip repos API -> 403 (out of scope) pull-robot GET lab-director/pi manifest -> 200 (public project, anonymous) push-robot POST platform/test blob-upload -> 202 (push works) push-robot POST platform/forgejo-runner upload -> 202 (push works) push-robot POST platform/docs blob-upload -> 202 (push works) push-robot POST lab-director/pi blob-upload -> 202 (push works) push-robot GET platform/docs manifest -> 200 (pull works) push-robot POST sendrast/e2e-runner blob-upload -> 401 (out of scope) push-robot GET sendrast/e2e-runner manifest -> 401 (out of scope) push-robot GET paperclip repos API -> 403 (out of scope) ``` ## Out-of-band state already applied (not in Git) - Robot accounts created in Harbor; secrets stored in OpenBao `kv/app-credentials`: `forgejo_runner_harbor_pull_username` / `_pull_token` / `_push_username` / `_push_token` (read-back hash-verified; values never printed). - Repo secrets `HARBOR_PUSH_USERNAME` / `HARBOR_PUSH_PASSWORD` created on `Homelab/infra-core` (base64 via API). They are **inert until this PR merges**; rollback = delete those two repo secrets (plus the robots in Harbor). ## Rollout caution (read before merging) This changes a secret that **all 8 runner pods** consume. ArgoCD will sync the ExternalSecret after merge, but pods keep the config they were seeded with at start — **existing pods stay on the old credential until they cycle**. Until then their `{"auths":{}}`-stripped configs remain stripped, so private pre-pulls stay broken on those pods regardless of this PR. Operator sequence (safe; no restart performed by the agent): 1. Merge this PR; let ArgoCD sync `external-secrets-config` and confirm `kubectl get externalsecret -n forgejo-runner runner-harbor-robot -o jsonpath='{.status.conditions[0]}'` → `SecretSynced`. 2. Confirm the synced Secret changed (`kubectl get secret runner-harbor-robot -n forgejo-runner -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r '.auths|keys[]'` → `harbor.aaron.reynoza.org`; the username inside is now `robot$forgejo-runner-pull`). 3. Cycle the runner pods at a maintenance moment — a deliberate fleet action (8 pods, `kubectl rollout restart statefulset/forgejo-runner -n forgejo-runner`). Pods re-run the `docker-config` initContainer and re-seed from the new secret. Registration state lives on per-pod PVCs, so a cycle is safe. 4. Verify per ticket: on a restarted pod, `stat -c %y /home/runner/.docker/config.json` mtime must **not** move after a push run of either fixed workflow (that is the proof the logout footgun is gone). ## Verification done here (offline) - `yamllint -d relaxed` clean of new errors; both workflow YAMLs parse; the modified run steps pass `bash -n`. - `kubectl apply --dry-run=server -f core/manifests/external-secrets/es-forgejo-runner.yaml` → schema valid, no write. - The ESO `dockerconfigjson` template renders valid JSON with the new values (auth round-trips). - OpenBao read-back hash-compare for all four new keys. - Live workflow runs / mtime proof / pre-pull success **cannot** be verified before merge + pod cycle — see rollout steps. ## Not done (deliberately, with rationale) - `pi-image-build.yaml` untouched (its `DOCKER_CONFIG` pattern is already correct and the brief scoped it out) — it still uses `HARBOR_*`, so retiring that admin-grade repo secret needs a follow-up that migrates it to `HARBOR_PUSH_*` (the robot already covers `lab-director/pi` so that follow-up is a one-line secret rename). Its header comment still describes `HARBOR_*` as a shared admin, which will be stale after the swap. - The same class of bug exists in other repos on the same runner labels (full API sweep of all 30 accessible repos, default + `live` branches): - `Sendrast/api-sendrast` — `ci-app.yml:133`, `release.yml:100`: unisolated `docker login` + `docker logout` (strips the entry). - `Sendrast/sendrast-web` — `e2e-image.yml:93`: unisolated `docker login` + `docker logout` (strips the entry). - `Homelab/lab-director` — `build.yaml` + `build-sandbox.yaml` (4 logins): unisolated `docker login`, no logout — it does not delete the entry but **overwrites** the pod-shared one with a lab-director push credential, which then breaks the daemon's sendrast pre-pull. Separate PR per repo; listed on the ticket rather than drive-by changed here. - The "guard" scope item (read-only mount / checksum alert) is **not** implemented: a read-only `/home/runner/.docker` would break every remaining `:host` workflow that still does a plain `docker login`, and a repo-local lint cannot protect other repos. Proposal recorded on the ticket.
HOMELAB-1735: fix(forgejo-runner): pull-only Harbor robot seed from OpenBao; stop workflows stripping it
Some checks failed
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 5s
Lint & Validate / shellcheck (pull_request) Successful in 3s
Lint & Validate / yaml-lint (pull_request) Successful in 10s
Lint & Validate / terraform-validate (pull_request) Has been cancelled
e53bfb50b8
es-forgejo-runner: the runner-harbor-robot seed (mounted into the runner
daemon's /home/runner/.docker and dind's /root/.docker) now reads a DEDICATED
pull-only robot from OpenBao kv/app-credentials instead of sharing
sendrast's own workload robot credential (kv/sendrast/harbor). Scope evidence:
sendrast is the only non-empty private project the runners touch; docker-cache
and platform are public and paperclip is private-but-empty.

runner-image-build.yaml + docs-build.yaml: log in to a throwaway DOCKER_CONFIG
and drop `docker logout`. The k8s/infra labels are :host mode, so a job's
`docker logout` deleted the pod-shared docker config entry, leaving every
runner pod unable to pre-pull private job images until restarted. Both now use
the platform-scoped push robot (repo secrets HARBOR_PUSH_*).
HOMELAB-1735: docs(ci): correct push-robot scope — platform + lab-director
All checks were successful
CI Review / helm-validate (pull_request) Successful in 5s
CI Review / receiver-test (pull_request) Successful in 6s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 10s
Lint & Validate / terraform-validate (pull_request) Successful in 2m49s
0/0 projects applied successfully.
4646fd7cc6
pi-image-build.yaml in this same repo publishes to lab-director/pi, so the
scoped push robot must cover lab-director as well; the comment now matches the
Harbor robot's actual project permissions.
Author
Owner

Landing worker report — NOT MERGED (1 verified blocker)

Convoy: HOMELAB-1735 landing. Review ran in fresh contexts (two independent pi -p --no-session reviewers, briefed only on the diff + ticket + repo contracts).

CI: green. PR head 4646fd7 → 6/6 checks success (CI Review / helm-validate, receiver-test; Lint & Validate / shellcheck, yaml-lint, terraform-validate; atlantis). Note: forgejo_ci_status 500s on actions/tasks (HOMELAB-1736) — obtained via the commits/{sha}/status fallback.

Repo secrets verified present (API, names/created_at only — no values read or printed):

HARBOR_USERNAME        created 2026-03-17T04:26:21Z
HARBOR_PASSWORD        created 2026-03-17T04:26:22Z
HARBOR_PUSH_USERNAME   created 2026-09-29T06:31:14Z
HARBOR_PUSH_PASSWORD   created 2026-09-29T06:31:14Z

Review verdicts

  • Reviewer 1 (workflows) — CLEAN. Mechanism correct, behaviourally identical to the proven pi-image-build.yaml pattern (same set -euo pipefail → export DOCKER_CONFIG="$(mktemp -d)" → login on stdin, no logout); no docker logout remains anywhere in .forgejo/workflows/**; bash guaranteed on both labels (one runner class, DESIRED_LABELS="infra:host,k8s:host", image ships bash); no cluster-mutating step; secrets hygiene clean.
  • Reviewer 2 (manifests) — BLOCKERS. ESO/consumer match verified clean (see below), but the change will not reach the cluster through the automated GitOps pipeline.

BLOCKER — the PR's only object-level change sits inside an ArgoCD-ignored path, so auto-sync will never apply it

prod apps/external-secrets-config.yaml:25-34 (the app that owns core/manifests/external-secrets, targetRevision: live, automated: {prune, selfHeal}):

  ignoreDifferences:
    - group: external-secrets.io
      kind: ExternalSecret
      jqPathExpressions:
        - .spec.data[].remoteRef      # <-- the whole remoteRef object
        - .spec.target.deletionPolicy

The complete object-level delta of this PR in that directory is spec.data[].remoteRef.{key,property} on runner-harbor-robot; everything else (secretStoreRef, target name/creationPolicy, template bytes, the other three ExternalSecrets) is identical base↔head.

Independently verified (not taken on trust):

  1. gitops-engine pkg/diff/diff.go — Diff() calls Normalize(config, …) and Normalize(live, …); Normalize() runs o.normalizer.Normalize(un), the ignoreDifferences normalizer. Both sides lose the ignored path → the resource compares equal → app reports Synced → automated sync is never triggered (selfHeal uses the same diff). When a sync does happen, ignored fields are applied (hence ArgoCD offers RespectIgnoreDifferences to opt out) — the point is that nothing triggers one.
  2. This exact footgun is already documented in-repo: prod apps/omniroute.yaml:42-53 — "the former broad .spec.data[].remoteRef path also swallowed NEW data entries and property changes" — and that app was migrated to field-scoped paths. apps/external-secrets-config.yaml was not.
  3. Live state right now (read-only): app external-secrets-config = Synced/Healthy, last operation 2026-09-22T04:25:37Z; live ES spec still key: kv/sendrast/harbor, property: robot_username/robot_token. No ES in that directory has changed in Git since 2026-09-22, so there is no pending OutOfSync to piggyback on.
  4. Why HOMELAB-1424 phase 3 landed while this will not: that cutover also changed the non-ignored secretStoreRef (kubernetes-secrets → openbao) → OutOfSync → full apply, which incidentally applied the new remoteRefs.

Impact. After merge the in-cluster runner-harbor-robot ExternalSecret keeps the old source, so the dedicated pull-only robot never lands and the security objective (runner pods no longer carrying the sendrast namespace's workload credential) is silently not achieved. The rollout step 1 in the PR body (SecretSynced) is a false pass — the old source still resolves and syncs fine. Only step 2 (the username inside the Secret must be robot$forgejo-runner-pull) would catch it, and only after pods cycle, which would re-seed the old credential.

Remediation options (operator decision — this is why the convoy stopped):

  1. Durable (recommended): prod PR narrowing the ignore to field-scoped paths, mirroring apps/omniroute.yaml:50-54 (.spec.data[].remoteRef.conversionStrategy|decodingStrategy|metadataPolicy + .spec.target.deletionPolicy). Then this PR's property change becomes visible → OutOfSync → ArgoCD applies it from Git, no manual sync. Trade-off: that app then compares key/property for ~20 ExternalSecrets across namespaces; any live drift would be patched by selfHeal (git becomes truth) — a GitOps config change with blast radius wider than this PR, so it needs your sign-off, and it is not in this PR's diff.
  2. Immediate, one-off: after merge, force a sync — argocd app sync external-secrets-config — then assert kubectl get externalsecret -n forgejo-runner runner-harbor-robot -o jsonpath='{.spec.data[*].remoteRef.key}' prints app-credentials app-credentials before cycling pods. Refresh alone moves status.sync.revision without applying — it looks like success and is not.
  3. In-PR forcing change: touch a non-ignored field/annotation on that ExternalSecret so the object is visibly changed. Works, but papers over a broken ignore and is arguably churn against option 1.

What merging anyway would still deliver (context for sequencing): the workflow half — the footgun fix and the HARBOR_PUSH_* swap go live for this repo's two workflows on merge, and because runner-image-build.yaml lists itself in on.push.paths, the merge itself triggers a real publish run (an immediate end-to-end test of the push robot + the mktemp pattern). The pull-robot decoupling is the part that does not land.

Everything else on the frozen constraints checked out

  • C1 no plaintext anywhere in the diff (scanned added lines for token-like strings: none); staged paths only; password piped on stdin, never argv.
  • C2 new push steps are the same three constructs in the same order as pi-image-build.yaml:128-132, no logout left; the set -e also removes the "failed mktemp silently falls back to $HOME/.docker" hole.
  • C3 pure source swap confirmed: target name runner-harbor-robot unchanged (= consumer's secretName, statefulset.yaml:256-258), .dockerconfigjson key unchanged (= consumer's items[].key, :260-262), template bytes identical base↔head, and the template still renders because the data[].secretKey names it interpolates (username/password) are unchanged. key: app-credentials resolves to kv/data/app-credentials under the store's path: kv + version: v2, matching the canonical form already used by es-harbor.yaml, es-grafana.yaml, es-forgejo.yaml, es-zitadel.yaml, es-rocketchat.yaml. (The removed kv/sendrast/harbor prefixed form was not broken — ESO strips and re-appends the provider path — so this is a genuine source swap, not a path repair.)
  • C4 no scope creep: exactly 4 files, pi-image-build.yaml untouched (still HARBOR_*), no guard mounts, no other repos.
  • C5 merge restarts nothing: the statefulset.yaml delta is 3 comment lines → no rendered-object change → no rollout; the ES is initContainer-seeded only, so un-cycled pods keep their current configs.
  • CR-1 (adopted from reviewer 1): the comment in both workflows says HARBOR_PUSH_* "replaces the all-projects admin-grade HARBOR_* credential for this repo's publishes" — not true yet, pi-image-build.yaml:131-132 still logs in with HARBOR_*/HARBOR_PASSWORD for its lab-director/pi push (the PR body already discloses this).
  • CR-2: statefulset.yaml:126 still says the emptyDir "keeps it writable for :host jobs' docker login", three lines above the new "job steps must never write here". Contradictory docs for the next reader.
  • CR-3: the throwaway DOCKER_CONFIG dir is never removed (inherited from the proven pattern), so a push token stays readable in /tmp for the pod's lifetime; trap 'rm -rf "${DOCKER_CONFIG}"' EXIT would close it — deliberately not requested, since C2 asks for behavioural identity with pi-image-build.yaml.
  • CR-4: ESO failure is quiet by design (on provider error it returns before touching the target Secret). If a property name is ever wrong/empty, the pod's .dockerconfigjson gets an empty username/password and private pre-pulls fail unauthorized again — i.e. the exact "flake in someone else's job" symptom. Consider alerting on the ESO status metric (ServiceMonitor is already enabled) — follow-up, not this PR.
  • CR-5 (docs, optional): the ESO rotation note should add "keep the old robot valid until every runner pod has been cycled" (the seed is copied once per pod and never re-read), and the scope-evidence note could state that a new private project pulled by these runners needs its own pull scope.

No cluster mutations were performed by this convoy; no pods were restarted. Ticket HOMELAB-1735 stays In Progress, blocked on the remediation choice above.

## Landing worker report — NOT MERGED (1 verified blocker) Convoy: HOMELAB-1735 landing. Review ran in fresh contexts (two independent `pi -p --no-session` reviewers, briefed only on the diff + ticket + repo contracts). **CI: green.** PR head `4646fd7` → 6/6 checks success (`CI Review / helm-validate`, `receiver-test`; `Lint & Validate / shellcheck`, `yaml-lint`, `terraform-validate`; atlantis). Note: `forgejo_ci_status` 500s on `actions/tasks` (HOMELAB-1736) — obtained via the `commits/{sha}/status` fallback. **Repo secrets verified present** (API, names/created_at only — no values read or printed): ``` HARBOR_USERNAME created 2026-03-17T04:26:21Z HARBOR_PASSWORD created 2026-03-17T04:26:22Z HARBOR_PUSH_USERNAME created 2026-09-29T06:31:14Z HARBOR_PUSH_PASSWORD created 2026-09-29T06:31:14Z ``` ### Review verdicts - **Reviewer 1 (workflows) — CLEAN.** Mechanism correct, behaviourally identical to the proven `pi-image-build.yaml` pattern (same `set -euo pipefail` → `export DOCKER_CONFIG="$(mktemp -d)"` → login on stdin, no logout); no `docker logout` remains anywhere in `.forgejo/workflows/**`; bash guaranteed on both labels (one runner class, `DESIRED_LABELS="infra:host,k8s:host"`, image ships bash); no cluster-mutating step; secrets hygiene clean. - **Reviewer 2 (manifests) — BLOCKERS.** ESO/consumer match verified clean (see below), but the change **will not reach the cluster through the automated GitOps pipeline**. --- ### BLOCKER — the PR's only object-level change sits inside an ArgoCD-ignored path, so auto-sync will never apply it `prod` `apps/external-secrets-config.yaml:25-34` (the app that owns `core/manifests/external-secrets`, `targetRevision: live`, `automated: {prune, selfHeal}`): ```yaml ignoreDifferences: - group: external-secrets.io kind: ExternalSecret jqPathExpressions: - .spec.data[].remoteRef # <-- the whole remoteRef object - .spec.target.deletionPolicy ``` The complete object-level delta of this PR in that directory is `spec.data[].remoteRef.{key,property}` on `runner-harbor-robot`; everything else (secretStoreRef, target name/creationPolicy, template bytes, the other three ExternalSecrets) is identical base↔head. **Independently verified (not taken on trust):** 1. `gitops-engine` `pkg/diff/diff.go` — `Diff()` calls `Normalize(config, …)` **and** `Normalize(live, …)`; `Normalize()` runs `o.normalizer.Normalize(un)`, the ignoreDifferences normalizer. Both sides lose the ignored path → the resource compares equal → app reports **Synced** → `automated` sync is never triggered (`selfHeal` uses the same diff). When a sync *does* happen, ignored fields are applied (hence ArgoCD offers `RespectIgnoreDifferences` to opt out) — the point is that nothing triggers one. 2. This exact footgun is already documented in-repo: `prod` `apps/omniroute.yaml:42-53` — *"the former broad `.spec.data[].remoteRef` path also swallowed NEW data entries and **property changes**"* — and that app was migrated to field-scoped paths. `apps/external-secrets-config.yaml` was not. 3. Live state right now (read-only): app `external-secrets-config` = **Synced/Healthy**, last operation `2026-09-22T04:25:37Z`; live ES spec still `key: kv/sendrast/harbor`, `property: robot_username/robot_token`. No ES in that directory has changed in Git since 2026-09-22, so there is no pending OutOfSync to piggyback on. 4. Why HOMELAB-1424 phase 3 landed while this will not: that cutover also changed the non-ignored `secretStoreRef` (`kubernetes-secrets` → `openbao`) → OutOfSync → full apply, which incidentally applied the new remoteRefs. **Impact.** After merge the in-cluster `runner-harbor-robot` ExternalSecret keeps the old source, so the dedicated pull-only robot never lands and the security objective (runner pods no longer carrying the `sendrast` namespace's workload credential) is silently not achieved. The rollout step 1 in the PR body (`SecretSynced`) is a **false pass** — the old source still resolves and syncs fine. Only step 2 (the username inside the Secret must be `robot$forgejo-runner-pull`) would catch it, and only *after* pods cycle, which would re-seed the **old** credential. **Remediation options (operator decision — this is why the convoy stopped):** 1. **Durable (recommended): prod PR narrowing the ignore to field-scoped paths**, mirroring `apps/omniroute.yaml:50-54` (`.spec.data[].remoteRef.conversionStrategy|decodingStrategy|metadataPolicy` + `.spec.target.deletionPolicy`). Then this PR's property change becomes visible → OutOfSync → ArgoCD applies it from Git, no manual sync. Trade-off: that app then compares `key`/`property` for ~20 ExternalSecrets across namespaces; any live drift would be patched by `selfHeal` (git becomes truth) — a GitOps config change with blast radius wider than this PR, so it needs your sign-off, and it is not in this PR's diff. 2. **Immediate, one-off: after merge, force a sync** — `argocd app sync external-secrets-config` — then assert `kubectl get externalsecret -n forgejo-runner runner-harbor-robot -o jsonpath='{.spec.data[*].remoteRef.key}'` prints `app-credentials app-credentials` **before** cycling pods. Refresh alone moves `status.sync.revision` without applying — it looks like success and is not. 3. **In-PR forcing change**: touch a non-ignored field/annotation on that ExternalSecret so the object is visibly changed. Works, but papers over a broken ignore and is arguably churn against option 1. **What merging anyway *would* still deliver** (context for sequencing): the workflow half — the footgun fix and the `HARBOR_PUSH_*` swap go live for this repo's two workflows on merge, and because `runner-image-build.yaml` lists itself in `on.push.paths`, the merge itself triggers a real publish run (an immediate end-to-end test of the push robot + the mktemp pattern). The pull-robot decoupling is the part that does not land. ### Everything else on the frozen constraints checked out - **C1** no plaintext anywhere in the diff (scanned added lines for token-like strings: none); staged paths only; password piped on stdin, never argv. - **C2** new push steps are the same three constructs in the same order as `pi-image-build.yaml:128-132`, no logout left; the `set -e` also removes the "failed `mktemp` silently falls back to `$HOME/.docker`" hole. - **C3** pure source swap confirmed: target name `runner-harbor-robot` unchanged (= consumer's `secretName`, `statefulset.yaml:256-258`), `.dockerconfigjson` key unchanged (= consumer's `items[].key`, `:260-262`), template bytes identical base↔head, and the template still renders because the `data[].secretKey` names it interpolates (`username`/`password`) are unchanged. `key: app-credentials` resolves to `kv/data/app-credentials` under the store's `path: kv` + `version: v2`, matching the canonical form already used by `es-harbor.yaml`, `es-grafana.yaml`, `es-forgejo.yaml`, `es-zitadel.yaml`, `es-rocketchat.yaml`. (The removed `kv/sendrast/harbor` prefixed form was *not* broken — ESO strips and re-appends the provider path — so this is a genuine source swap, not a path repair.) - **C4** no scope creep: exactly 4 files, `pi-image-build.yaml` untouched (still `HARBOR_*`), no guard mounts, no other repos. - **C5** merge restarts nothing: the `statefulset.yaml` delta is 3 comment lines → no rendered-object change → no rollout; the ES is initContainer-seeded only, so un-cycled pods keep their current configs. - CR-1 (adopted from reviewer 1): the comment in both workflows says `HARBOR_PUSH_*` "replaces the all-projects admin-grade HARBOR_* credential for this repo's publishes" — not true yet, `pi-image-build.yaml:131-132` still logs in with `HARBOR_*`/`HARBOR_PASSWORD` for its `lab-director/pi` push (the PR body already discloses this). - CR-2: `statefulset.yaml:126` still says the emptyDir "keeps it writable for :host jobs' docker login", three lines above the new "job steps must never write here". Contradictory docs for the next reader. - CR-3: the throwaway `DOCKER_CONFIG` dir is never removed (inherited from the proven pattern), so a push token stays readable in `/tmp` for the pod's lifetime; `trap 'rm -rf "${DOCKER_CONFIG}"' EXIT` would close it — deliberately not requested, since C2 asks for behavioural identity with `pi-image-build.yaml`. - CR-4: ESO failure is quiet by design (on provider error it returns before touching the target Secret). If a property name is ever wrong/empty, the pod's `.dockerconfigjson` gets an empty username/password and private pre-pulls fail `unauthorized` again — i.e. the exact "flake in someone else's job" symptom. Consider alerting on the ESO status metric (ServiceMonitor is already enabled) — follow-up, not this PR. - CR-5 (docs, optional): the ESO rotation note should add "keep the old robot valid until every runner pod has been cycled" (the seed is copied once per pod and never re-read), and the scope-evidence note could state that a *new* private project pulled by these runners needs its own pull scope. **No cluster mutations were performed by this convoy; no pods were restarted.** Ticket HOMELAB-1735 stays **In Progress**, blocked on the remediation choice above.
Author
Owner

BLOCKER DISPOSITIONED — resolved by Homelab/prod #795 (merged)

The blocker from the landing review above was correct and is now fixed. Nothing in this PR changed; the fix landed upstream in the repo that owned the footgun.

Unblocking change — Homelab/prod#795, merged as 40185c32 into live (PR: Homelab/prod#795):

prod/apps/external-secrets-config.yaml no longer ignores the broad .spec.data[].remoteRef. It now ignores the field-scoped set, byte-identical to the two in-repo precedents (apps/omniroute.yaml, apps/sendrast.yaml):

        - .spec.data[].remoteRef.conversionStrategy
        - .spec.data[].remoteRef.decodingStrategy
        - .spec.data[].remoteRef.metadataPolicy
        - .spec.target.deletionPolicy
        - .spec.target.template.mergePolicy

remoteRef.key / .property are therefore diff-compared again, so this PR's only object-level change becomes visible and triggers a sync.

Verified live, after #795 merged (read-only kubectl)

Assertion Observed
ArgoCD adopted the narrowed list within ~75s: spec.ignoreDifferences[0].jqPathExpressions = the 5-path list
App state unchanged by #795 alone (no-op as predicted) Synced / Healthy
This PR's change not yet applied — pre-merge baseline live ES remoteRef.key still kv/sendrast/harbor ×2

Pre-merge drift census re-derived independently: 20 ExternalSecrets / 42 spec.data[] entries in the app's source path, git↔live identical on remoteRef.key+.property for 42/42 (0 mismatches), no dataFrom, remoteRef.version/.sourceRef declared on neither side — so #795 provably could not change the diff set, only un-hide future remoteRef edits.

Partial-automation note: repo secret HARBOR_PUSH_USERNAME/HARBOR_PUSH_PASSWORD were confirmed present and this head is green 6/6. The credential itself is inert for the pods until they cycle (pods keep the config seeded at start); the operator sequence at the end of the PR body still stands and is tracked on HOMELAB-1735.

Merging now.

## BLOCKER DISPOSITIONED — resolved by Homelab/prod #795 (merged) The blocker from the landing review above was correct and is now fixed. Nothing in **this** PR changed; the fix landed upstream in the repo that owned the footgun. **Unblocking change — `Homelab/prod#795`, merged as `40185c32` into `live`** (PR: https://forgejo.aaron.reynoza.org/Homelab/prod/pulls/795): `prod/apps/external-secrets-config.yaml` no longer ignores the broad `.spec.data[].remoteRef`. It now ignores the field-scoped set, byte-identical to the two in-repo precedents (`apps/omniroute.yaml`, `apps/sendrast.yaml`): ```yaml - .spec.data[].remoteRef.conversionStrategy - .spec.data[].remoteRef.decodingStrategy - .spec.data[].remoteRef.metadataPolicy - .spec.target.deletionPolicy - .spec.target.template.mergePolicy ``` `remoteRef.key` / `.property` are therefore diff-compared again, so this PR's only object-level change becomes visible and triggers a sync. ### Verified live, after #795 merged (read-only kubectl) | Assertion | Observed | |---|---| | ArgoCD adopted the narrowed list | within ~75s: `spec.ignoreDifferences[0].jqPathExpressions` = the 5-path list | | App state unchanged by #795 alone (no-op as predicted) | `Synced` / `Healthy` | | This PR's change *not yet* applied — pre-merge baseline | live ES `remoteRef.key` still `kv/sendrast/harbor` ×2 | Pre-merge drift census re-derived independently: 20 ExternalSecrets / 42 `spec.data[]` entries in the app's source path, git↔live identical on `remoteRef.key`+`.property` for **42/42** (0 mismatches), no `dataFrom`, `remoteRef.version`/`.sourceRef` declared on neither side — so #795 provably could not change the diff set, only un-hide future remoteRef edits. **Partial-automation note:** repo secret `HARBOR_PUSH_USERNAME`/`HARBOR_PUSH_PASSWORD` were confirmed present and this head is green 6/6. The credential itself is inert for the *pods* until they cycle (pods keep the config seeded at start); the operator sequence at the end of the PR body still stands and is tracked on HOMELAB-1735. Merging now.
aaron merged commit dd99f5c637 into live 2026-09-29 07:49:03 +00:00
Author
Owner

Landing report — MERGED dd99f5c6 (infra-core live) — with a FAILED publish run

Merged on green CI: 4646fd7cc → commit status success, 6/6. Merge commit dd99f5c637461d889b3a182980861940e049066d, merged 2026-09-29T07:49:07Z.

ESO half — PASS (the blocker fix works end-to-end)

Prerequisite Homelab/prod#795 had already merged (40185c32) so the app no longer ignored spec.data[].remoteRef.{key,property}. Read-only assertions after merge:

Assertion Observed
App external-secrets-config OutOfSync at t+255s → auto-synced → Synced / Healthy at t+270s (well inside the 15-min window)
ES condition type=Ready status=True reason=**SecretSynced** (this ESO version reports the SecretSynced reason on a Ready condition), refreshTime=2026-09-29T07:53:47Z
spec.data[*].remoteRef — both entries username -> app-credentials / forgejo_runner_harbor_pull_username
password -> app-credentials / forgejo_runner_harbor_pull_token
Synced Secret actually re-materialised .dockerconfigjson 202 → 216 bytes; auths = ["harbor.aaron.reynoza.org"]; embedded username sha256 matches robot$forgejo-runner-pull (verified by hash comparison — no value printed)

The pull-only robot is now the real credential in the cluster, and the sendrast workload credential is no longer the source. The review blocker is confirmed resolved.


ANOMALY — the runner-image publish run FAILED; no image was published

The Build Runner Image workflow did fire on the merge (its own on.push.paths lists the workflow file), but it failed:

run id 5674 | index_in_repo 1490 | workflow runner-image-build.yaml | event push
status=failure | commit dd99f5c637461d889b3a182980861940e049066d
started 07:49:05Z  stopped 07:51:23Z  duration 2m18s
https://forgejo.aaron.reynoza.org/Homelab/infra-core/actions/runs/1490

Step breakdown (from the job page): Build image success (2m15s) → Push to Harbor FAILURE (0s) → Print immutable digest skipped.

Log evidence (actions/runs/1490/jobs/0/attempt/1/logs):

07:51:22.646  ---> c4b937863ace
07:51:22.661  Successfully built c4b937863ace
07:51:22.661  Successfully tagged harbor.aaron.reynoza.org/platform/forgejo-runner:dd99f5c
07:51:22.740  Error response from daemon: Get "https://harbor.aaron.reynoza.org/v2/": unauthorized:
07:51:22.742  ⚙️ [runner]: RUN exit status 1

The failure is at docker login, not at the push. The image was built and tagged, then the step died 0.08s later — nowhere near enough time for the ~250 MB multi-layer push this image requires. Critically, the successful run of the same workflow on 2026-09-22 (index 1463) logged, at the identical point:

03:58:45.609  WARNING! Your password will be stored unencrypted in /home/runner/.docker/config.json.
03:58:45.610  Login Succeeded
...
03:59:08.833  090c8d9: digest: sha256:871d62066ecdfab334ea77031baa5ba21c887d9f6f735aaacb40072c8db7fdf4
🏁  Job succeeded

Neither the WARNING! nor Login Succeeded line appears in the failed run. So docker login was rejected by Harbor before any credential was stored.

Confirmed no image was published (crane, the PR's own tool surface):

crane ls harbor.aaron.reynoza.org/platform/forgejo-runner
  -> 090c8d9, 71b1838, 764644c, bcea3e4, f7b246e, hl1419-1, hl988-1, latest
crane manifest .../forgejo-runner:dd99f5c
  -> NOT_FOUND: artifact platform/forgejo-runner:dd99f5c not found

090c8d9 is the 2026-09-22 build; no new runner image has been published since then. The trailing latest tag likewise did not move to this merge.

Where the problem is (not elsewhere)

  • Repo secret names resolve correctly — repos/Homelab/infra-core/actions/secrets returns HARBOR_PUSH_USERNAME and HARBOR_PUSH_PASSWORD, both created_at 2026-09-29T06:31:14Z. (The API never returns values, so values could not be checked.)
  • The pod-shared stripped {"auths":{}} config is not the cause: this step now uses a throwaway DOCKER_CONFIG, and docker login writes client-side only.
  • Base images are public (code.forgejo.org/forgejo/runner:12.13.1, debian:bookworm-slim), so the successful Build image step needed no Harbor auth.
  • The PR body's own registry probes reported the push robot working (202 on platform/forgejo-runner), which points at the value copied into the repo secrets rather than the robot's existence or scope.

Leading hypothesis: the HARBOR_PUSH_USERNAME value is missing the robot$ prefix (Harbor system robot usernames are robot$<name>), or the username/token pair was transposed when copying from OpenBao kv/app-credentials. HARBOR_PUSH_USERNAME should equal kv/app-credentials.forgejo_runner_harbor_push_username verbatim.

Not remediated by design — this landing was scoped to the two-PR sequence; no fix was pushed and nothing in the cluster was mutated. The HARBOR_* secrets are still present, so a revert of the two workflow files would restore the previous publishing path.

Still outstanding

pi-image-build.yaml was deliberately left on HARBOR_* (noted in the PR body) and is unaffected by this failure. The operator pod-cycle for the pull credential remains pending — the 8 runner pods still read 16 bytes ({"auths":{}}) at /home/runner/.docker/config.json (runner-0 mtime 09-25 05:27, runner-4 09-29 04:03:37, runner-7 09-29 04:07:57), so the pre-pull fix is not yet effective on them.

## Landing report — MERGED `dd99f5c6` (infra-core `live`) — with a FAILED publish run Merged on green CI: `4646fd7cc` → commit status **success, 6/6**. Merge commit `dd99f5c637461d889b3a182980861940e049066d`, merged `2026-09-29T07:49:07Z`. ### ESO half — PASS (the blocker fix works end-to-end) Prerequisite `Homelab/prod#795` had already merged (`40185c32`) so the app no longer ignored `spec.data[].remoteRef.{key,property}`. Read-only assertions after merge: | Assertion | Observed | |---|---| | App `external-secrets-config` | `OutOfSync` at `t+255s` → auto-synced → **`Synced` / `Healthy`** at **`t+270s`** (well inside the 15-min window) | | ES condition | `type=Ready status=True reason=**SecretSynced**` (this ESO version reports the `SecretSynced` reason on a `Ready` condition), `refreshTime=2026-09-29T07:53:47Z` | | `spec.data[*].remoteRef` — **both** entries | `username -> app-credentials` / `forgejo_runner_harbor_pull_username`<br>`password -> app-credentials` / `forgejo_runner_harbor_pull_token` | | Synced Secret actually re-materialised | `.dockerconfigjson` **202 → 216 bytes**; `auths = ["harbor.aaron.reynoza.org"]`; embedded username sha256 **matches `robot$forgejo-runner-pull`** (verified by hash comparison — no value printed) | The pull-only robot is now the real credential in the cluster, and the `sendrast` workload credential is no longer the source. **The review blocker is confirmed resolved.** --- ## ANOMALY — the runner-image publish run FAILED; no image was published The `Build Runner Image` workflow did fire on the merge (its own `on.push.paths` lists the workflow file), but it **failed**: ``` run id 5674 | index_in_repo 1490 | workflow runner-image-build.yaml | event push status=failure | commit dd99f5c637461d889b3a182980861940e049066d started 07:49:05Z stopped 07:51:23Z duration 2m18s https://forgejo.aaron.reynoza.org/Homelab/infra-core/actions/runs/1490 ``` Step breakdown (from the job page): `Build image` **success (2m15s)** → `Push to Harbor` **FAILURE (0s)** → `Print immutable digest` skipped. Log evidence (`actions/runs/1490/jobs/0/attempt/1/logs`): ``` 07:51:22.646 ---> c4b937863ace 07:51:22.661 Successfully built c4b937863ace 07:51:22.661 Successfully tagged harbor.aaron.reynoza.org/platform/forgejo-runner:dd99f5c 07:51:22.740 Error response from daemon: Get "https://harbor.aaron.reynoza.org/v2/": unauthorized: 07:51:22.742 ⚙️ [runner]: RUN exit status 1 ``` **The failure is at `docker login`, not at the push.** The image was built and tagged, then the step died 0.08s later — nowhere near enough time for the ~250 MB multi-layer push this image requires. Critically, the successful run of the *same* workflow on 2026-09-22 (index 1463) logged, at the identical point: ``` 03:58:45.609 WARNING! Your password will be stored unencrypted in /home/runner/.docker/config.json. 03:58:45.610 Login Succeeded ... 03:59:08.833 090c8d9: digest: sha256:871d62066ecdfab334ea77031baa5ba21c887d9f6f735aaacb40072c8db7fdf4 🏁 Job succeeded ``` Neither the `WARNING!` nor `Login Succeeded` line appears in the failed run. So `docker login` was rejected by Harbor before any credential was stored. **Confirmed no image was published** (`crane`, the PR's own tool surface): ``` crane ls harbor.aaron.reynoza.org/platform/forgejo-runner -> 090c8d9, 71b1838, 764644c, bcea3e4, f7b246e, hl1419-1, hl988-1, latest crane manifest .../forgejo-runner:dd99f5c -> NOT_FOUND: artifact platform/forgejo-runner:dd99f5c not found ``` `090c8d9` is the 2026-09-22 build; **no new runner image has been published since then**. The trailing `latest` tag likewise did not move to this merge. ### Where the problem is (not elsewhere) - Repo secret **names resolve correctly** — `repos/Homelab/infra-core/actions/secrets` returns `HARBOR_PUSH_USERNAME` and `HARBOR_PUSH_PASSWORD`, both `created_at 2026-09-29T06:31:14Z`. (The API never returns values, so values could not be checked.) - The pod-shared stripped `{"auths":{}}` config is **not** the cause: this step now uses a throwaway `DOCKER_CONFIG`, and `docker login` writes client-side only. - Base images are public (`code.forgejo.org/forgejo/runner:12.13.1`, `debian:bookworm-slim`), so the successful `Build image` step needed no Harbor auth. - The PR body's own registry probes reported the push robot working (202 on `platform/forgejo-runner`), which points at the **value copied into the repo secrets** rather than the robot's existence or scope. **Leading hypothesis:** the `HARBOR_PUSH_USERNAME` value is missing the `robot$` prefix (Harbor system robot usernames are `robot$<name>`), or the username/token pair was transposed when copying from OpenBao `kv/app-credentials`. `HARBOR_PUSH_USERNAME` should equal `kv/app-credentials.forgejo_runner_harbor_push_username` verbatim. **Not remediated** by design — this landing was scoped to the two-PR sequence; no fix was pushed and nothing in the cluster was mutated. The `HARBOR_*` secrets are still present, so a revert of the two workflow files would restore the previous publishing path. ### Still outstanding `pi-image-build.yaml` was deliberately left on `HARBOR_*` (noted in the PR body) and is unaffected by this failure. The operator pod-cycle for the pull credential remains pending — the 8 runner pods still read `16 bytes` (`{"auths":{}}`) at `/home/runner/.docker/config.json` (`runner-0` mtime 09-25 05:27, `runner-4` 09-29 04:03:37, `runner-7` 09-29 04:07:57), so the pre-pull fix is not yet effective on them.
Sign in to join this conversation.
No reviewers
No labels
operator-review
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
Homelab/infra-core!521
No description provided.