feat(sec): provision the narrow conformance CI trigger credential -- and refuse to arm a fake green #19

Merged
jakman85 merged 3 commits from feat/conformance-trigger-kubeconfig into main 2026-09-30 03:58:48 +00:00
Owner

Rebased 2026-09-29 — the DECISIONS-LOG.md append is why this PR was unmergeable

This PR no longer touches DECISIONS-LOG.md at all. The handoff it used to append to that
file's tail now lives in its own file, decisions/2026-09-29-conformance-trigger-kubeconfig.md.

Why: DECISIONS-LOG.md was append-only and every branch appended to the same last hunk, so no
two concurrent branches could auto-merge. Measured against the live Gitea on 2026-09-29, four of
the five open PRs were mergeable: false and DECISIONS-LOG.md was the only conflicting path
in all four. This PR (#19) was one of them. Nothing was wrong with the change itself — the log's
storage format serialised work that was otherwise parallel. The workflow fix is PR #24
(one entry per file under decisions/, with the old file frozen and its SHA-256 pinned).

What changed in the rebase:

  • Rebuilt on today's main (cadf64a). infra/gitea/README.md and
    infra/gitea/conformance-trigger-kubeconfig.sh are byte-identical to the original three
    commits — verified with git diff against the old head.
  • The DECISIONS-LOG.md hunk (~99 lines) is gone; git diff origin/main...HEAD --name-only now
    lists only decisions/2026-09-29-conformance-trigger-kubeconfig.md,
    infra/gitea/README.md, infra/gitea/conformance-trigger-kubeconfig.sh.
  • The original commit that touched only DECISIONS-LOG.md (the measurement that this PR's CI has
    no approval-policy check) was dropped as a commit; its substance is carried into the new entry
    verbatim in meaning, including every UNVERIFIED it recorded. Nothing was upgraded into a
    claim. The script is still UNEXECUTED.
  • git merge-tree --write-tree origin/main HEAD reports no conflict, and this PR merges
    cleanly against today's main on its own — it does not depend on #24 landing first (it also
    merge-tree's clean against #24's branch, since the paths are disjoint).

Gate re-run after the rebase (pinned toolbox, agent worktree, DEV_KUBECONFIG=/dev/null plus
the ENGINE bind-mount shim): MERGE GATE: UNDETERMINED, rc 3, diff base
origin/main @ c59fe9f923c4 ... HEAD @ ad5de2dba9fe, touched requirement R-ENV-01.
spec-immutability, decisions-log, lint, l0, render-golden, l1 all green; the gate
decision names exactly one stage that did not run — gitea-settings (SETTINGS_API_TOKEN
unwired repo-wide). The conformance stage also reported SKIPPED (CLUSTER_AVAILABLE!=true,
Phase 0). PR #24 merged to main while this rework was in flight, so the new decisions-log
stage did run, and it validated this PR's entry: "archive frozen, 2 entries well-formed", with
DECISIONS-LOG.md unchanged and matching decisions/ARCHIVE.sha256.
rc 3 is UNDETERMINED, not green. This supersedes the gate paragraph further down, which was
written before the rebase and reported spec-immutability as a second skip.

Note: this branch is now rebased onto c59fe9f (post-#24 main), not cadf64a.


Implements docs/plans/l2-l3-conformance-ci-wiring.md §3 item 6 (= §5 step 7) as an operator script: infra/gitea/conformance-trigger-kubeconfig.sh, plus its section in infra/gitea/README.md and a handoff entry (now decisions/2026-09-29-conformance-trigger-kubeconfig.md — see the rebase note at the top).

It is UNEXECUTED. Minting a cluster credential and changing Gitea variables are operator actions. Nothing was minted, no Gitea object was written, no mutating kubectl was run. Everything below is a claim about the script, not about the platform.

What it does

Mints a token for conformance/conformance-trigger (TokenRequest), wraps only that token with the cluster CA and server into a kubeconfig, stores it as the Actions secret CONFORMANCE_TRIGGER_KUBECONFIG, and sets vars.CLUSTER_AVAILABLE only as a separately-requested, precondition-gated, TTY-confirmed final step.

Never a cluster-admin kubeconfig, never a copy of the operator's own — only .cluster fields are read from the operator context; its client cert/key are not copied. The token is never printed and never reaches argv: it goes into a 0600 file in a 0700 temp dir and into the PUT body file (--data @file). The script refuses to run under set -x, because xtrace would print it.

The two traps this repo already paid for

  • GITEA_ is a reserved secret-name prefix (HTTP 400 invalid secret name). The name is CONFORMANCE_TRIGGER_KUBECONFIG, which is also what gate.yml and merge-gate.yaml already reference.
  • curl -sS exits 0 on an HTTP error. Every write checks %{http_code} and reads the object back. The secret read-back proves presence by name only — Gitea never returns secret values — and the script says so rather than implying its content was checked. The variable read-back asserts the value is exactly true, which is what gate.yml compares against.

Preconditions are split in two, and the split is the security argument

Class Checks Blocks
mint-blocking (cluster) SA/Role/RoleBinding present; ClusterPolicy conformance-trigger-job-shape present + Enforce + failurePolicy: Fail + Ready the mint — nothing is created
arm-blocking (repo) digest-pinned CONFORMANCE_RUNNER_IMAGE; initContainers + the admitted image in chainsaw-job.yaml; CONFORMANCE_TRIGGER_KUBECONFIG in ci/conformance.sh code, not a comment only CLUSTER_AVAILABLE

Mint-blocking, because Kubernetes places no use check on a Job's serviceAccountName: a create jobs token in that namespace is the privileged conformance-runner identity unless admission bounds its shape. RBAC cannot close that (plan §4); minting before the boundary is live hands CI the escalation the boundary exists to close.

Arm-blocking separately, because storing the secret early is harmless (the job is gated off) and is the plan's own order.

All three arm-blocking checks fail in the tree today, so a live run would store the secret and then refuse to arm CLUSTER_AVAILABLE, naming each missing item. That is intended: measured 2026-09-29, CLUSTER_AVAILABLE=true with no trigger path scaffold-skipped all 54 tests at rc 0 and the stage printed "L2/L3 conformance passed". ci/conformance.sh now blocks that case, so arming early does not enable L2/L3 — it only breaks every PR. Arming needs the explicit flag, every precondition green, and a typed confirmation on a TTY.

The credential is proven narrow before anything is stored

auth whoami must return system:serviceaccount:conformance:conformance-trigger; a real get jobs round-trip must succeed; then an auth can-i matrix asserts both halves — granted (create/get jobs, get pods, get pods/log) and denied (delete jobs, create pods/exec, get secrets, create jobs in default, get nodes, '*' '*' --all-namespaces). A mismatch aborts before any store, and the message says explicitly not to widen conformance/incluster/rbac.yaml to make it pass. That file is untouched here.

Unverified, because it was not run

  • Only bash -n (clean) and careful reading. shellcheck is not on the host and the gate has no shellcheck stage.
  • The Gitea variables API shape (POST creates / PUT updates, body field value — not data as for secrets) is from documentation, not measurement. The script tries POST, falls back to PUT, prints the response body on failure, and treats the read-back as the authority.
  • Kyverno readiness is read from either .status.ready or a Ready condition; "neither present" blocks rather than assuming ready.
  • Whether the embedded API-server URL is reachable from the runner pod cannot be known from here — hence --server, and a hard refusal on a loopback address.
  • The token's real lifetime is reported by decoding the JWT exp claim rather than trusting --duration (the API server caps it) — that decode path is also unexercised.

Gate (pinned toolbox, from the agent worktree)

MERGE GATE: UNDETERMINED, rc 3, on this exact commit. lint, l0, render-golden, l1 green; touched requirement R-ENV-01. Stages that did not run: spec-immutability (sibling spec repo absent — the documented tolerated skip) and gitea-settings (SETTINGS_API_TOKEN is a credential this agent does not hold). Neither was "fixed"; gate.yml tolerates exit 3.

Measured CI on this PR (not assumed)

gate / gate (pull_request)          success   Successful in 1m34s
gate / conformance (pull_request)   pending   Blocked by required conditions

The brief for this work expected an approval-policy check to FAIL because infra/gitea/** is security-relevant. No such check exists on main — .gitea/workflows/ holds only gate.yml and the dormant merge-gate.yaml, and ci/approval-policy.sh is absent. Since confirmed by the coordinator: it is added by PR #17, still unmerged. So today nothing machine-enforces "security-relevant paths need a human"; that rule rests on required_approvals: 1 plus a reviewer reading the diff. Re-measure once #17 lands — this PR touches infra/gitea/** and would then be exactly the case that check is for.

A green gate tick is not a claim that this script works: the gate never executes infra/gitea/**, and the L2/L3 stage skipped. Read the "Unverified" section above.

Also in this PR: the README's illegal secret name (commit 3)

infra/gitea/README.md still named GITEA_SETTINGS_TOKEN — the name PR #15 replaced with SETTINGS_API_TOKEN precisely because Gitea reserves the GITEA_ prefix and rejects it with HTTP 400 invalid secret name. The file documenting that story was still repeating the illegal name, in its behaviour table and in its closing "remaining step" line. Both fixed, folded in here because this PR already owns that file.

The note records what ci/gitea-settings.sh actually does, quoted from the file rather than assumed:

TOKEN="${SETTINGS_API_TOKEN:-${GITEA_SETTINGS_TOKEN:-}}"

So GITEA_SETTINGS_TOKEN is still honoured, but only as an environment variable for a local run — it can never be the CI wiring, because the secret that would feed it cannot be created at all. Documentation only; no behaviour change.

Noted, not fixed — same defect, different file

ci/gitea-settings.sh:45 still instructs the reader to "create a Gitea repo secret GITEA_SETTINGS_TOKEN", which is impossible for the same reason. That file is outside this PR's diff, so it is left for a one-line change rather than widening a security-reviewed diff.

🤖 Generated with Claude Code

## Rebased 2026-09-29 — the `DECISIONS-LOG.md` append is why this PR was unmergeable **This PR no longer touches `DECISIONS-LOG.md` at all.** The handoff it used to append to that file's tail now lives in its own file, `decisions/2026-09-29-conformance-trigger-kubeconfig.md`. Why: `DECISIONS-LOG.md` was append-only and every branch appended to the *same last hunk*, so no two concurrent branches could auto-merge. Measured against the live Gitea on 2026-09-29, four of the five open PRs were `mergeable: false` and `DECISIONS-LOG.md` was the **only** conflicting path in all four. This PR (#19) was one of them. Nothing was wrong with the change itself — the log's storage format serialised work that was otherwise parallel. The workflow fix is PR #24 (one entry per file under `decisions/`, with the old file frozen and its SHA-256 pinned). What changed in the rebase: - Rebuilt on today's `main` (`cadf64a`). `infra/gitea/README.md` and `infra/gitea/conformance-trigger-kubeconfig.sh` are **byte-identical** to the original three commits — verified with `git diff` against the old head. - The `DECISIONS-LOG.md` hunk (~99 lines) is gone; `git diff origin/main...HEAD --name-only` now lists only `decisions/2026-09-29-conformance-trigger-kubeconfig.md`, `infra/gitea/README.md`, `infra/gitea/conformance-trigger-kubeconfig.sh`. - The original commit that touched *only* `DECISIONS-LOG.md` (the measurement that this PR's CI has no `approval-policy` check) was dropped as a commit; its substance is carried into the new entry verbatim in meaning, **including every `UNVERIFIED` it recorded**. Nothing was upgraded into a claim. The script is still **UNEXECUTED**. - `git merge-tree --write-tree origin/main HEAD` reports **no conflict**, and this PR merges cleanly against today's `main` on its own — it does not depend on #24 landing first (it also merge-tree's clean against #24's branch, since the paths are disjoint). **Gate re-run after the rebase** (pinned toolbox, agent worktree, `DEV_KUBECONFIG=/dev/null` plus the `ENGINE` bind-mount shim): `MERGE GATE: UNDETERMINED`, **rc 3**, diff base `origin/main @ c59fe9f923c4` ... `HEAD @ ad5de2dba9fe`, touched requirement `R-ENV-01`. `spec-immutability`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1` all **green**; the gate decision names exactly one stage that did not run — **`gitea-settings`** (`SETTINGS_API_TOKEN` unwired repo-wide). The `conformance` stage also reported SKIPPED (`CLUSTER_AVAILABLE!=true`, Phase 0). PR #24 merged to `main` while this rework was in flight, so the new **`decisions-log`** stage did run, and it validated this PR's entry: *"archive frozen, 2 entries well-formed"*, with `DECISIONS-LOG.md` unchanged and matching `decisions/ARCHIVE.sha256`. **rc 3 is UNDETERMINED, not green.** This supersedes the gate paragraph further down, which was written before the rebase and reported `spec-immutability` as a second skip. > Note: this branch is now rebased onto `c59fe9f` (post-#24 `main`), not `cadf64a`. --- Implements `docs/plans/l2-l3-conformance-ci-wiring.md` §3 item 6 (= §5 step 7) as an operator script: `infra/gitea/conformance-trigger-kubeconfig.sh`, plus its section in `infra/gitea/README.md` and a handoff entry (now `decisions/2026-09-29-conformance-trigger-kubeconfig.md` — see the rebase note at the top). **It is UNEXECUTED.** Minting a cluster credential and changing Gitea variables are operator actions. Nothing was minted, no Gitea object was written, no mutating `kubectl` was run. Everything below is a claim about the script, not about the platform. ## What it does Mints a token for `conformance/conformance-trigger` (TokenRequest), wraps **only that token** with the cluster CA and server into a kubeconfig, stores it as the Actions secret `CONFORMANCE_TRIGGER_KUBECONFIG`, and sets `vars.CLUSTER_AVAILABLE` only as a separately-requested, precondition-gated, TTY-confirmed final step. Never a cluster-admin kubeconfig, never a copy of the operator's own — only `.cluster` fields are read from the operator context; its client cert/key are not copied. The token is never printed and never reaches `argv`: it goes into a 0600 file in a 0700 temp dir and into the PUT **body file** (`--data @file`). The script refuses to run under `set -x`, because xtrace would print it. ## The two traps this repo already paid for - **`GITEA_` is a reserved secret-name prefix** (HTTP 400 `invalid secret name`). The name is `CONFORMANCE_TRIGGER_KUBECONFIG`, which is also what `gate.yml` and `merge-gate.yaml` already reference. - **`curl -sS` exits 0 on an HTTP error.** Every write checks `%{http_code}` **and** reads the object back. The secret read-back proves presence **by name only** — Gitea never returns secret values — and the script says so rather than implying its content was checked. The variable read-back asserts the value is exactly `true`, which is what `gate.yml` compares against. ## Preconditions are split in two, and the split is the security argument | Class | Checks | Blocks | |---|---|---| | **mint-blocking** (cluster) | SA/Role/RoleBinding present; ClusterPolicy `conformance-trigger-job-shape` present + `Enforce` + `failurePolicy: Fail` + Ready | the **mint** — nothing is created | | **arm-blocking** (repo) | digest-pinned `CONFORMANCE_RUNNER_IMAGE`; `initContainers` + the admitted image in `chainsaw-job.yaml`; `CONFORMANCE_TRIGGER_KUBECONFIG` in `ci/conformance.sh` **code, not a comment** | only `CLUSTER_AVAILABLE` | Mint-blocking, because Kubernetes places **no `use` check on a Job's `serviceAccountName`**: a `create jobs` token in that namespace **is** the privileged `conformance-runner` identity unless admission bounds its shape. RBAC cannot close that (plan §4); minting before the boundary is live hands CI the escalation the boundary exists to close. Arm-blocking separately, because storing the secret early is harmless (the job is gated off) and is the plan's own order. **All three arm-blocking checks fail in the tree today**, so a live run would store the secret and then **refuse to arm `CLUSTER_AVAILABLE`**, naming each missing item. That is intended: measured 2026-09-29, `CLUSTER_AVAILABLE=true` with no trigger path scaffold-skipped all 54 tests at rc 0 and the stage printed "L2/L3 conformance passed". `ci/conformance.sh` now blocks that case, so arming early does not enable L2/L3 — it only breaks every PR. Arming needs the explicit flag, every precondition green, **and** a typed confirmation on a TTY. ## The credential is proven narrow before anything is stored `auth whoami` must return `system:serviceaccount:conformance:conformance-trigger`; a real `get jobs` round-trip must succeed; then an `auth can-i` matrix asserts **both halves** — granted (`create/get jobs`, `get pods`, `get pods/log`) and **denied** (`delete jobs`, `create pods/exec`, `get secrets`, `create jobs` in `default`, `get nodes`, `'*' '*' --all-namespaces`). A mismatch aborts before any store, and the message says explicitly **not** to widen `conformance/incluster/rbac.yaml` to make it pass. That file is untouched here. ## Unverified, because it was not run - Only `bash -n` (clean) and careful reading. `shellcheck` is not on the host and the gate has no shellcheck stage. - The Gitea **variables** API shape (POST creates / PUT updates, body field `value` — not `data` as for secrets) is from documentation, not measurement. The script tries POST, falls back to PUT, prints the response body on failure, and treats the read-back as the authority. - Kyverno readiness is read from either `.status.ready` or a `Ready` condition; "neither present" **blocks** rather than assuming ready. - Whether the embedded API-server URL is reachable from the runner pod cannot be known from here — hence `--server`, and a hard refusal on a loopback address. - The token's real lifetime is reported by decoding the JWT `exp` claim rather than trusting `--duration` (the API server caps it) — that decode path is also unexercised. ## Gate (pinned toolbox, from the agent worktree) `MERGE GATE: UNDETERMINED`, **rc 3**, on this exact commit. `lint`, `l0`, `render-golden`, `l1` green; touched requirement `R-ENV-01`. Stages that did not run: `spec-immutability` (sibling spec repo absent — the documented tolerated skip) and `gitea-settings` (`SETTINGS_API_TOKEN` is a credential this agent does not hold). Neither was "fixed"; `gate.yml` tolerates exit 3. ### Measured CI on this PR (not assumed) gate / gate (pull_request) success Successful in 1m34s gate / conformance (pull_request) pending Blocked by required conditions The brief for this work expected an `approval-policy` check to FAIL because `infra/gitea/**` is security-relevant. **No such check exists on `main`** — `.gitea/workflows/` holds only `gate.yml` and the dormant `merge-gate.yaml`, and `ci/approval-policy.sh` is absent. Since confirmed by the coordinator: it is added by **PR #17, still unmerged**. So today nothing machine-enforces "security-relevant paths need a human"; that rule rests on `required_approvals: 1` plus a reviewer reading the diff. Re-measure once #17 lands — this PR touches `infra/gitea/**` and would then be exactly the case that check is for. A green `gate` tick is **not** a claim that this script works: the gate never executes `infra/gitea/**`, and the L2/L3 stage skipped. Read the "Unverified" section above. ## Also in this PR: the README's illegal secret name (commit 3) `infra/gitea/README.md` still named `GITEA_SETTINGS_TOKEN` — the name PR #15 replaced with `SETTINGS_API_TOKEN` precisely because Gitea reserves the `GITEA_` prefix and rejects it with HTTP 400 `invalid secret name`. The file documenting that story was still repeating the illegal name, in its behaviour table and in its closing "remaining step" line. Both fixed, folded in here because this PR already owns that file. The note records what `ci/gitea-settings.sh` **actually** does, quoted from the file rather than assumed: TOKEN="${SETTINGS_API_TOKEN:-${GITEA_SETTINGS_TOKEN:-}}" So `GITEA_SETTINGS_TOKEN` is still honoured, but **only as an environment variable for a local run** — it can never be the CI wiring, because the secret that would feed it cannot be created at all. Documentation only; no behaviour change. ## Noted, not fixed — same defect, different file `ci/gitea-settings.sh:45` still instructs the reader to *"create a Gitea repo secret `GITEA_SETTINGS_TOKEN`"*, which is impossible for the same reason. That file is outside this PR's diff, so it is left for a one-line change rather than widening a security-reviewed diff. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added 1 commit 2026-09-30 00:14:25 +00:00
feat(sec): provision the narrow conformance CI trigger credential -- and refuse to arm a fake green
gate / gate (pull_request) Successful in 1m34s
gate / conformance (pull_request) Has been skipped
5a006aeb15
docs/plans/l2-l3-conformance-ci-wiring.md §3 item 6, as an operator script:
infra/gitea/conformance-trigger-kubeconfig.sh. UNEXECUTED here -- minting a
cluster credential and changing Gitea variables are operator actions.

It mints a token for conformance/conformance-trigger (TokenRequest), wraps ONLY
that token with the cluster CA and server into a kubeconfig, and stores it as the
Actions secret CONFORMANCE_TRIGGER_KUBECONFIG. Never cluster-admin, never a copy
of the operator's own kubeconfig. The token is never printed and never reaches
argv: it goes into a 0600 file and into the PUT body FILE (--data @file), and the
script refuses to run under `set -x` because xtrace would print it.

Two traps this repo already paid for are designed around:

  * GITEA_ is a RESERVED secret-name prefix (HTTP 400 "invalid secret name"), so
    the name is CONFORMANCE_TRIGGER_KUBECONFIG -- which is also what gate.yml and
    merge-gate.yaml already reference.
  * `curl -sS` exits 0 on an HTTP error. Every write checks %{http_code} AND reads
    the object back. The secret read-back proves presence by NAME only (Gitea
    never returns secret values) and the script says so rather than implying its
    content was checked; the variable read-back asserts the value is exactly
    "true", which is what gate.yml compares against.

PRECONDITIONS ARE SPLIT IN TWO, and the split is the security argument:

  mint-blocking (cluster) -- SA/Role/RoleBinding present, and ClusterPolicy
  conformance-trigger-job-shape present + Enforce + failurePolicy Fail + Ready.
  Any failure and NOTHING IS MINTED. Kubernetes places no `use` check on a Job's
  serviceAccountName, so a `create jobs` token in that namespace IS the privileged
  conformance-runner identity unless admission bounds its shape. RBAC cannot close
  that (plan §4); minting before the boundary is live hands CI the escalation the
  boundary exists to close.

  arm-blocking (repo) -- digest-pinned CONFORMANCE_RUNNER_IMAGE, an initContainer
  plus the admitted image in chainsaw-job.yaml, and CONFORMANCE_TRIGGER_KUBECONFIG
  referenced in ci/conformance.sh CODE rather than a comment. These block only
  vars.CLUSTER_AVAILABLE, because storing the secret early is harmless (the job is
  gated off) and is the plan's own order.

All three arm-blocking checks FAIL in the tree today, so a live run would store
the secret and then REFUSE to arm CLUSTER_AVAILABLE, naming each missing item.
That is the intended behaviour: measured 2026-09-29, CLUSTER_AVAILABLE=true with
no trigger path scaffold-skipped all 54 tests at rc 0 and the stage printed "L2/L3
conformance passed". ci/conformance.sh now blocks that case, so arming early does
not enable L2/L3 -- it only breaks every PR. Arming needs the explicit flag, every
precondition green, and a typed confirmation on a TTY.

Before storing anything, the credential is PROVEN narrow: auth whoami must be
system:serviceaccount:conformance:conformance-trigger, a real `get jobs` round-trip
must succeed, and an auth can-i matrix asserts both halves -- granted (create/get
jobs, get pods, get pods/log) and DENIED (delete jobs, create pods/exec, get
secrets, create jobs in default, get nodes, '*' '*' --all-namespaces). A mismatch
aborts before any store, and says explicitly not to widen
conformance/incluster/rbac.yaml to make it pass.

Verified only by `bash -n` (clean) and reading. The Gitea variables API shape
(POST creates, PUT updates, body field `value` not `data`) is from documentation,
not measurement: the script tries POST then PUT, prints the response body on
failure, and treats the read-back as the authority. Kyverno readiness is accepted
from either .status.ready or a Ready condition, and "neither present" blocks
rather than assuming ready.

Gate (pinned toolbox): MERGE GATE: UNDETERMINED (rc 3) -- spec-immutability and
gitea-settings unrun, the expected local result.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin added 1 commit 2026-09-30 00:17:25 +00:00
docs(decisions-log): PR #19's CI has no approval-policy check -- measured, not assumed
gate / gate (pull_request) Successful in 1m35s
gate / conformance (pull_request) Has been skipped
64944aeda0
The brief for the trigger-credential work said PR #19 would FAIL `approval-policy`
because infra/gitea/** is security-relevant. Measured on the head commit's
/statuses: `gate / gate (pull_request)` succeeded in 1m34s and the conformance
context is the usual blocked placeholder. .gitea/workflows/ holds only gate.yml
and the dormant merge-gate.yaml -- there is no path-scoped approval policy at all.

So the "security-relevant paths need a human" rule is carried entirely by
required_approvals: 1 plus a reviewer reading the diff, not by a check. Recorded
because this is the same shape as every other §2b finding: a control believed to
exist because prose said it did.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin added 1 commit 2026-09-30 00:23:42 +00:00
docs(gitea): the README still named the ILLEGAL secret GITEA_SETTINGS_TOKEN
gate / gate (pull_request) Successful in 1m36s
gate / conformance (pull_request) Has been skipped
9275dae6e0
PR #15 renamed the Actions secret to SETTINGS_API_TOKEN because Gitea RESERVES the
GITEA_ prefix and rejects such a name with HTTP 400 "invalid secret name" -- the
defect that left the branch-protection detector inert and unnoticed. The README in
this very directory, which documents that story, went on naming the illegal name in
its behaviour table and in its closing "remaining step" line.

Both now say SETTINGS_API_TOKEN, plus a note recording what ci/gitea-settings.sh
ACTUALLY does, quoted from the file rather than assumed:

    TOKEN="${SETTINGS_API_TOKEN:-${GITEA_SETTINGS_TOKEN:-}}"

GITEA_SETTINGS_TOKEN is still honoured, but ONLY as an environment variable for a
local run -- it can never be the CI wiring, because the secret that would feed it
cannot be created at all. The closing line also predated PR #15 storing the secret;
it now says so.

Documentation only. No behaviour change.

NOT fixed here, because it is outside this PR's diff: ci/gitea-settings.sh:45 still
instructs the reader to "create a Gitea repo secret GITEA_SETTINGS_TOKEN", which is
impossible. Same defect, different file -- worth its own one-line change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin force-pushed feat/conformance-trigger-kubeconfig from 9275dae6e0 to 7f5a32f629 2026-09-30 03:46:53 +00:00 Compare
gitea-admin force-pushed feat/conformance-trigger-kubeconfig from 7f5a32f629 to ad5de2dba9 2026-09-30 03:52:37 +00:00 Compare
jakman85 approved these changes 2026-09-30 03:58:36 +00:00
jakman85 merged commit 762c3833fc into main 2026-09-30 03:58:48 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#19