diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 7e2d2971f..1b54d36af 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -38,7 +38,8 @@ name: Build ErsatzTV Image # report `success` in seconds — the two REQUIRED contexts (`Build & test (.NET)`, `EF migration # integrity (SQLite + MySql)`) must keep reporting or a docs-only PR could never merge. We do NOT # `if:`-skip a required job: on Gitea 1.25.4 a skipped job reports commit-status state `skipped` -# (verified, throwaway PR #418) and we don't rely on how branch protection treats a skipped +# (verified, throwaway PR #418; re-confirmed on 1.27.1, 2026-08-28, ersatztv#747 — `Build & push +# image (amd64)` still reports `skipped` on every PR head) and we don't rely on how branch protection treats a skipped # REQUIRED context. See docs/ci-cd.md -> "Docs-only skip". # # RELEASE-PATH DELIMITER GATE (ersatztv#767): the `scan` job runs the PyYAML-based delimiter-ban diff --git a/docs/ci-cd.md b/docs/ci-cd.md index f2f0fb235..472c9e34d 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -761,8 +761,9 @@ The jobs are **not** `if:`-skipped. That is deliberate and it is the whole trap or an `if:`-skipped job), those contexts would never report and the PR could **never merge** — the naive fix *bricks* docs PRs rather than speeding them up. - On Gitea **1.25.4** an `if:`-skipped job reports commit-status state **`skipped`**, a distinct - state (verified with a throwaway probe, PR #418) — not `success`. We do **not** rely on how branch - protection treats a `skipped` **required** context. Keeping the job running and gating its *steps* + state (verified with a throwaway probe, PR #418) — not `success`. **Still true on 1.27.1** + (re-confirmed 2026-08-28, ersatztv#747, from production PR heads rather than a new probe PR). We do + **not** rely on how branch protection treats a `skipped` **required** context. Keeping the job running and gating its *steps* makes the required context report `success` unconditionally, which is safe by construction. - Non-required jobs may skip freely: production already proves a `skipped` **non-required** context does not block merge (`build` is `skipped` on every PR). So `build` skips its image steps on a @@ -1300,10 +1301,13 @@ entire gate was skippable with no forgery at all, which was cheaper than every r - `block_admin_merge_override: true` — without it (the default is `false`), a repo admin could `POST /pulls/{n}/merge` with `force_merge: true` and merge straight past a missing or red `review-verdict/h10`. Disabling push alone just moves the bypass from the push path to the merge - path, since `timothy` is admin and is the identity every session already uses. **Source-attested, - not probed** (Gitea 1.27 `CanBypassBranchProtection`): verifying it by experiment means merging an - unreviewed PR, so the field was set rather than measured. Setting it is safe under either - semantics; re-confirming the bypass itself rides with ersatztv#747. + path, since `timothy` is admin and is the identity every session already uses. **Measured + 2026-08-28 (ersatztv#747)**, replacing the earlier source-attested-only note: on throwaway + `scratch/747{a,b}-base` rules, `false` let an admin `force_merge` past an ABSENT required context + (HTTP 200) and `true` refused the identical call (HTTP 405 `Not all required status checks + successful`). It never required merging an unreviewed PR into `main` — a scratch BASE branch carries + its own rule, so the experiment is disposable. `enable_bypass_allowlist` (new since 1.25.4) is + **not** a substitute: enabled with an empty username list it left the admin force-merge working. **Operator recovery when a required context gets stuck.** `block_admin_merge_override: true` removes the "Merge (admin)" / `force_merge: true` escape that used to unstick a PR whose required context was @@ -1567,6 +1571,14 @@ measured rather than assumed. Gitea 1.25.4 auto-cancels superseded `push` runs o statusless with nothing left to re-trigger it. Full measurements and the two surviving residuals: `ci.verdict-write-retarget-fence`. +The `push`-supersession half of that claim stays **1.25.4-dated on purpose** (ersatztv#747, +2026-08-28). It is not merely unchecked, it is no longer observable on this repo: no workflow triggers +on a push to a non-`main` branch (`ci-image.yml` and `docker-build.yml` filter their `push` trigger to +`main` plus `v*` tags; the other four have no `push` trigger at all), and `main` itself refuses direct +pushes since `release.main-direct-push-disabled`. So reproducing it would mean adding a throwaway +workflow trigger, which costs more than the claim is worth — nothing branches on it. The +`pull_request_target` half is what the fence relies on, and that is separately measured at 1.27.1. + Separately, after posting an exemption `success` the job re-reads the per-POST status history and, if a human `Review-verdict:` row appeared during the write window, overwrites its own status with `pending` and logs an error. **That check runs only for a `success` write**, so "a human `BLOCKED` can diff --git a/docs/decisions/records/ci/actions-credential-scoping.md b/docs/decisions/records/ci/actions-credential-scoping.md index cf2ae2cb7..69c7b6a0b 100644 --- a/docs/decisions/records/ci/actions-credential-scoping.md +++ b/docs/decisions/records/ci/actions-credential-scoping.md @@ -139,7 +139,12 @@ The instance was upgraded mid-session on 2026-08-05 (#743). Everything above mea therefore a *dated* claim, not a current one: the scope enum, the `reqRepoWriter` gate and the 403 probe were all taken pre-upgrade and have NOT been re-run. They are recorded honestly as of their date and are the best evidence available, but do not cite them as current behaviour without -re-probing. Re-verification of the 1.25.4-pinned claims across the CI docs is tracked separately. +re-probing. #747 carried out that re-verification across the CI docs on 2026-08-28 and **did not** +discharge this record's remaining items: the `v1.25.4` scope enum, the `reqRepoWriter` gate and the +`write:package` 403 were left deliberately un-re-run, because each needs a scoped PAT minted against +the live instance rather than a read-only probe. They stay 1.25.4-dated on purpose. What #747 did +settle elsewhere is the merge-path half — see `release.main-direct-push-disabled` for the measured +`block_admin_merge_override` semantics and for why `enable_bypass_allowlist` is not a substitute. **Surviving routes — this record is not a mitigation for any of them.** `RENOVATE_TOKEN` is a `write:repository` bot PAT in the same store and still cannot be scoped down, because Renovate needs diff --git a/docs/decisions/records/ci/batch-pushes-no-cancel-route.md b/docs/decisions/records/ci/batch-pushes-no-cancel-route.md index d8fe67cbd..8cca4a37f 100644 --- a/docs/decisions/records/ci/batch-pushes-no-cancel-route.md +++ b/docs/decisions/records/ci/batch-pushes-no-cancel-route.md @@ -1,19 +1,23 @@ --- key: ci.batch-pushes-no-cancel-route -title: '2026-07-21 — Batch your pushes: there is no agent-side cancel route on Gitea 1.25.4 (#542)' +title: '2026-07-21 — Batch your pushes: there is no agent-side cancel route on Gitea 1.25.4 or 1.27.1 (#542)' status: active since: '2026-07-21' supersedes: none superseded-by: none rule: Hold review fixes, doc corrections and format fixes locally and push **once** — a superseded run cannot be cancelled from the agent side and holds a runner slot until it finishes. -signals: 'cancel run 404 · Gitea 1.25.4 · `POST /api/v1/.../actions/runs/{id}/cancel` · MCP `actions_run_write` `cancel_run` · session+CSRF · `dispatch_workflow` · orphaned run · 4-slot runner · paths: n/a · issues: #542' +signals: 'cancel run 404 · Gitea 1.25.4 · Gitea 1.27.1 · `POST /api/v1/.../actions/runs/{id}/cancel` · MCP `actions_run_write` `cancel_run` · session+CSRF · `dispatch_workflow` · orphaned run · 4-slot runner · paths: n/a · issues: #542' mechanics: Gitea Actions REST + MCP `actions_run_write`; operator-only cancel in the browser UI. --- -Cancellation is impossible from the agent side on this Gitea (**1.25.4**): -`POST /api/v1/.../actions/runs/{id}/cancel` returns **404**, MCP `actions_run_write`'s `cancel_run` -returns **404**, and the web-UI route needs a session + CSRF that does not script (login 303s with no -session cookie). **Only the operator can cancel, in the browser** — so if you must supersede a live +Cancellation is impossible from the agent side on this Gitea, measured on **1.25.4** and +**re-confirmed on 1.27.1, 2026-08-28 (#747)**: `POST /api/v1/.../actions/runs/{id}/cancel` returns +**404**, MCP `actions_run_write`'s `cancel_run` returns **404**, and the web-UI route needs a session ++ CSRF that does not script (login 303s with no session cookie). The 1.27.1 re-probe also read the +live swagger, so the absence is structural rather than one 404: the only POST routes under +`actions/runs/{run}` are `rerun`, `rerun-failed-jobs` and `jobs/{job_id}/rerun`. There is a +`DELETE /actions/runs/{run}`, which removes a finished run's record — it is not a cancel and does not +free a runner slot. **Only the operator can cancel, in the browser** — so if you must supersede a live run, say so explicitly instead of leaving it burning. (`dispatch_workflow` is a different route and still works for re-triggering a **main** run.) diff --git a/docs/decisions/records/ci/docs-only-skip-steps.md b/docs/decisions/records/ci/docs-only-skip-steps.md index bde330652..8bfd595c6 100644 --- a/docs/decisions/records/ci/docs-only-skip-steps.md +++ b/docs/decisions/records/ci/docs-only-skip-steps.md @@ -20,7 +20,10 @@ docs-only PR produced **no run** for them, those contexts never report and the P — the naive fix bricks docs PRs instead of speeding them. A probe (throwaway PR #418) confirmed that on Gitea **1.25.4** an `if:`-skipped job reports commit-status state **`skipped`** (a distinct state, not `success`); how branch protection treats a `skipped` *required* context is not something we rely -on. +on. **Re-confirmed unchanged on 1.27.1, 2026-08-28 (#747)**, without a new probe PR — `Build & push +image (amd64)` is `if:`-skipped on every PR, and reports `skipped` on the heads of PR #829 +(`e5a01dc84100`) and PR #828 (`02bb0393`) alongside `success` and `pending` on the rest. The distinct +state survived the upgrade, so the reason this decision gates STEPS rather than jobs still holds. **The decision.** Each heavy job (`test`, `migrations`, `functional-e2e`, `build`) runs `scripts/ci-detect-docs-only.sh` as its first post-checkout step (`id: detect`) and gates every real diff --git a/docs/decisions/records/ci/exemption-provenance.md b/docs/decisions/records/ci/exemption-provenance.md index fd0c4fc24..6f5b62806 100644 --- a/docs/decisions/records/ci/exemption-provenance.md +++ b/docs/decisions/records/ci/exemption-provenance.md @@ -21,7 +21,9 @@ mid-flight. The job enumerated against the moved base, read docs-only, and poste reclassified** (`created_at == updated_at`), leaving a PR into `main` whose diff carried a C# file behind a green required check. Closed unmerged, branches deleted, no forged `h10` left anywhere. **Why a base BINDING and not a pinned diff.** Diffing two immutable shas would close it outright; -Gitea 1.25.4 cannot serve that — measured: `compare/{base}...{head}` returns no `files`, and a +Gitea cannot serve that — measured on 1.25.4 and **re-confirmed on 1.27.1, 2026-08-28 (#747)**: +`compare/{base}...{head}` returns an object whose only keys are `commits` and `total_commits`, no +`files` (probed on `b16ec15d6c1e...9b4844c1a8a9`, 5 commits). And a `--depth=1` fetch of the two shas has no merge base, so three-dot is impossible and two-dot over-reports everything `main` gained since the branch point. So the base is read before the first page and after the last, and **the gap is stated plainly**: a retarget opening *and* closing strictly diff --git a/docs/decisions/records/ci/gate-trigger-base-resolved.md b/docs/decisions/records/ci/gate-trigger-base-resolved.md index 8cdb12dc6..d1f2f90bf 100644 --- a/docs/decisions/records/ci/gate-trigger-base-resolved.md +++ b/docs/decisions/records/ci/gate-trigger-base-resolved.md @@ -24,7 +24,10 @@ branch protection requires the *context*, not an author, while carrying `require on this instance (Gitea 1.25.4) with **four** scratch PRs, not by analogy to GitHub: `pull_request` ran the head's rewrite and never wrote the real `h10`; `pull_request_target` ignored the identical rewrite and the base definition posted `h10=pending` on `opened` and `synchronize` alike, secrets available; -`branches: [main]` produced no run at all from a non-`main` base; and the fourth — the negative one +`branches: [main]` produced no run at all from a non-`main` base — **re-confirmed on 1.27.1, +2026-08-28 (#747)** as a by-product of an unrelated probe, where a PR from a scratch head into a +scratch base carried thirteen `pull_request` contexts and no review-verdict context of any kind; and +the fourth — the negative one establishing the residual below — is counted because omitting it turns an honest partial into an overclaim. Probes posted only probe-named contexts, never a forged `h10`. Full results in #699. diff --git a/docs/decisions/records/ci/shared-pr-file-enumeration.md b/docs/decisions/records/ci/shared-pr-file-enumeration.md index 696e3f4f1..c4c81972f 100644 --- a/docs/decisions/records/ci/shared-pr-file-enumeration.md +++ b/docs/decisions/records/ci/shared-pr-file-enumeration.md @@ -55,7 +55,12 @@ in each caller: - CR/LF rejection and `..` rejection on every path. - A closed `.status` allow-list — `added`/`deleted`/`changed`/`modified`/`renamed`/`copied`, not an - open denylist. Note `changed` and `deleted` are the values live Gitea 1.25.4 actually emits; + open denylist. The emitted vocabulary was re-derived on 1.27.1, 2026-08-28 (#747) from a + **constructed** corpus rather than a sample — every file row of the 200 most recently updated PRs, + because two arbitrary PRs had shown only `changed`/`added` and would have licensed a false negative. + Live 1.27.1 emits exactly four values: `changed` (1262), `added` (489), `renamed` (2), `deleted` (2). + All four are already in the allow-list, so the closed set is now evidenced rather than partly + presumed; the 1.25.4-dated note that only `changed` and `deleted` were observed understated it. `modified` is accepted alongside `changed` because a closed list built from the wrong vocabulary would gate every genuine docs-only PR. GitHub's `removed` is deliberately **not** in the list — an earlier draft of this record said it was, which would have sent a maintainer looking for a value diff --git a/docs/decisions/records/release/main-direct-push-disabled.md b/docs/decisions/records/release/main-direct-push-disabled.md index 818998d89..ccc711177 100644 --- a/docs/decisions/records/release/main-direct-push-disabled.md +++ b/docs/decisions/records/release/main-direct-push-disabled.md @@ -7,7 +7,7 @@ supersedes: none superseded-by: none rule: 'Branch protection on `main` carries `enable_push: false` AND `block_admin_merge_override: true`. Both halves are required and neither is sufficient. `enable_push: false` removes the direct-push path, leaving the PR merge path — the only path on which Gitea evaluates `status_check_contexts`, and therefore the only path on which `review-verdict/h10` is consulted at all. `block_admin_merge_override: true` then closes the force-merge bypass on that remaining path: with it false (the default), `CanBypassBranchProtection` returns true for a repo admin, so `POST /pulls/{n}/merge` with `force_merge: true` merges a PR whose `h10` is missing or red — one API call, no forgery, no PATCH. Do NOT "soften" the push half to a push WHITELIST: measured here, a whitelist naming `timothy` still admits the push, and `timothy` is the identity every agent session, PAT and injected `GITEA_TOKEN` already acts as, so the whitelist form closes nothing while reading in review as a control. Same reasoning is why the admin-override half is needed: an admin-shaped control that exempts the only admin exempts everybody. What remains open: a credential that can PATCH branch protection off can still undo either half — an accepted residual, not a closed route. Tag pushes are unaffected (`tag_protections` governs those separately), so the release cut still works.' signals: 'direct push to main, push whitelist, enable_push false, branch protection bypass, review-verdict/h10 bypassable without forging, merge consent derived not asserted, pre-receive hook declined, Not allowed to push to protected branch, protected branch, tag_protections, release tag push, GITEA_TOKEN repo write, RENOVATE_TOKEN, site admin bypass, PR-only flow · paths: `docs/ci-cd.md` · issues: #743, #697, #698, #622, #672, #706, #742, server-management#714' -mechanics: 'Gitea 1.27.1. `PATCH /api/v1/repos/timothy/ersatztv/branch_protections/main` with `{"enable_push": false, "block_admin_merge_override": true}`; whitelist fields left off (`enable_push_whitelist: false`, empty arrays), `enable_force_push: false`, `enable_merge_whitelist: false`, `required_approvals: 0`. MEASURED 2026-08-05 against a throwaway `probe-743-*` rule rather than against `main`: with `enable_push: false` a push by `timothy` (site admin) was REFUSED — `pre-receive hook declined`, `Not allowed to push to protected branch`; after PATCHing the same rule to `enable_push: true` + `enable_push_whitelist: true` + `push_whitelist_usernames: ["timothy"]` the identical push SUCCEEDED. Separately probed on a second throwaway rule: a contents-API write (`PUT /repos/{o}/{r}/contents/{path}` with `branch` set to the protected branch) was REFUSED HTTP 403 `user cannot commit to repo [user: timothy]` — so the web-editor/API file-write surface does not bypass it either. Then on `main` itself: `git push origin HEAD:main` REFUSED, and a tag-only push SUCCEEDED from the same worktree. `GET .../tag_protections` returns `[]`; repo is `fork: false`, `mirror: false`. NOT measured, source-attested only (Gitea 1.27 `CanBypassBranchProtection`, `services/pull/check.go`, `routers/private/hook_pre_receive.go`): that `block_admin_merge_override: false` would have let an admin `force_merge` past the required contexts — the field was set to true rather than probed, since probing it means merging an unreviewed PR. All probe artifacts (two rules, two branches, one tag) deleted and confirmed gone; `origin/main` head unchanged at `08e95f9ec` throughout.' +mechanics: 'Gitea 1.27.1. `PATCH /api/v1/repos/timothy/ersatztv/branch_protections/main` with `{"enable_push": false, "block_admin_merge_override": true}`; whitelist fields left off (`enable_push_whitelist: false`, empty arrays), `enable_force_push: false`, `enable_merge_whitelist: false`, `required_approvals: 0`. MEASURED 2026-08-05 against a throwaway `probe-743-*` rule rather than against `main`: with `enable_push: false` a push by `timothy` (site admin) was REFUSED — `pre-receive hook declined`, `Not allowed to push to protected branch`; after PATCHing the same rule to `enable_push: true` + `enable_push_whitelist: true` + `push_whitelist_usernames: ["timothy"]` the identical push SUCCEEDED. Separately probed on a second throwaway rule: a contents-API write (`PUT /repos/{o}/{r}/contents/{path}` with `branch` set to the protected branch) was REFUSED HTTP 403 `user cannot commit to repo [user: timothy]` — so the web-editor/API file-write surface does not bypass it either. Then on `main` itself: `git push origin HEAD:main` REFUSED, and a tag-only push SUCCEEDED from the same worktree. `GET .../tag_protections` returns `[]`; repo is `fork: false`, `mirror: false`. All probe artifacts (two rules, two branches, one tag) deleted and confirmed gone; `origin/main` head unchanged at `08e95f9ec` throughout. MEASURED 2026-08-28 on 1.27.1 (#747), superseding the earlier source-attested-only note on this field: on throwaway `scratch/747{a,b,c}-base` rules — never on `main`, whose rule `updated_at` is unchanged at 2026-08-05 throughout — an admin `force_merge: true` against an ABSENT required context MERGED (HTTP 200) with `block_admin_merge_override: false`, and the IDENTICAL call with only that field flipped to `true` was REFUSED HTTP 405 `Not all required status checks successful`. Probing it never required merging an unreviewed PR into `main`: a scratch BASE branch carries its own protection rule, so the whole experiment is disposable. Third arm, same method: `enable_bypass_allowlist: true` with an EMPTY `bypass_allowlist_usernames` did NOT constrain the admin — the force-merge still returned HTTP 200 — so the 1.27 bypass allowlist is not a substitute for `block_admin_merge_override`. All #747 probe artifacts (four rules, eight branches — a base/head pair per arm — and PRs #863-#866) deleted and confirmed gone; `origin/main` head unchanged at `b16ec15d6` throughout, and `main`''s own rule was never PATCHed.' --- **Why a whitelist was the wrong shape.** #743 proposed "a push whitelist on `main` (or disable direct @@ -31,6 +31,25 @@ repo admin and `POST /pulls/{n}/merge` with `force_merge: true` merged straight the only admin exempts everybody.** Both fields are now set; treat them as one control, and never cite `enable_push: false` alone as the reason the gate holds. +**Both halves are measured, and the 1.27 bypass allowlist is not a third option (#747).** The +admin-override half was set on source attestation alone, because probing it looked like it required +merging an unreviewed PR into `main`. It does not: a scratch BASE branch carries its own protection +rule, so the experiment is disposable and `main` is never touched. Measured that way on 2026-08-28, +with the control that makes it attributable — `block_admin_merge_override: false` let an admin +`force_merge` past an ABSENT required context (HTTP 200); flipping only that field to `true` refused +the identical call (HTTP 405). `enable_bypass_allowlist`, which did not exist on 1.25.4 and reads like +a better-targeted control, is **not** one: enabled with an empty username list it left the admin +force-merge working (HTTP 200). It answers "who may bypass when named", not "may an admin bypass at +all" — the implicit admin bypass is removed only by `block_admin_merge_override`. Do not swap them. + +**An absent required context BLOCKS, and `mergeable` does not tell you so.** Both halves of this were +asserted by our docs and unproven until #747. A required context that is never posted at all — the +state a PR is in whenever `review-verdict.yml` does not run — refuses the merge with HTTP 405 `Not all +required status checks successful`; the gate does not fail open on absence. The trap is the observable: +the PR API reported `mergeable: true` on that same PR while the merge was being refused, because that +field reflects conflict-freedom and ignores branch protection entirely. Never read `mergeable` to +decide whether the gate would admit a merge — attempt the merge, or read the required contexts. + **What this actually closes, and what it does not.** It closes the *write-only* credential routes, which is most of #743's own "who can do it" list: the injected `GITEA_TOKEN` (repo write at the time, narrowed to `code: read` in five of six workflows by #748; not admin either way), diff --git a/docs/decisions/workflow-process.md b/docs/decisions/workflow-process.md index c6a226366..ab400ed2b 100644 --- a/docs/decisions/workflow-process.md +++ b/docs/decisions/workflow-process.md @@ -96,7 +96,7 @@ another doc or an old issue comment should land here and then follow the link. - 2026-07-21 — An infra-shaped red under host load is not a code failure (#542) — [`ci.infra-shaped-red-under-load`](records/ci/infra-shaped-red-under-load.md) - 2026-07-21 — Arm the CI monitor at PR-open, via the commit-status endpoint (#542) — [`ci.monitor-armed-at-pr-open`](records/ci/monitor-armed-at-pr-open.md) - 2026-07-21 — BOM/format pre-push detection: use the `xxd` loop, and run `dotnet format --include` under `bash -c` (#542) — [`process.bom-format-detection-recipe`](records/process/bom-format-detection-recipe.md) -- 2026-07-21 — Batch your pushes: there is no agent-side cancel route on Gitea 1.25.4 (#542) — [`ci.batch-pushes-no-cancel-route`](records/ci/batch-pushes-no-cancel-route.md) +- 2026-07-21 — Batch your pushes: there is no agent-side cancel route on Gitea 1.25.4 or 1.27.1 (#542) — [`ci.batch-pushes-no-cancel-route`](records/ci/batch-pushes-no-cancel-route.md) - 2026-07-21 — Before deleting a "workaround", enumerate every behavior it provided (#542) — [`process.enumerate-workaround-behaviors-before-deleting`](records/process/enumerate-workaround-behaviors-before-deleting.md) - 2026-07-21 — Bound parallel builds by FREE RAM, never regenerate goldens (#542) — [`process.build-concurrency-limits`](records/process/build-concurrency-limits.md) - 2026-07-21 — Build and verify locally, then trust it; CI confirms (#542) — [`ci.verify-locally-ci-confirms`](records/ci/verify-locally-ci-confirms.md) diff --git a/scripts/ci-detect-docs-only.sh b/scripts/ci-detect-docs-only.sh index 01f046b13..11feb8581 100755 --- a/scripts/ci-detect-docs-only.sh +++ b/scripts/ci-detect-docs-only.sh @@ -13,7 +13,8 @@ # Why this is the merge-gate-safe half of #416: the two REQUIRED contexts (`Build & test (.NET)`, # `EF migration integrity (SQLite + MySql)`) are gated by SKIPPING STEPS inside a job that always # runs and always reports `success` — never by an `if:`-skipped job. On Gitea 1.25.4 an -# `if:`-skipped job reports commit-status state `skipped` (verified, PR #418), and how branch +# `if:`-skipped job reports commit-status state `skipped` (verified, PR #418; re-confirmed on +# 1.27.1, 2026-08-28, ersatztv#747), and how branch # protection treats a `skipped` REQUIRED context is not something we rely on. Non-required jobs may # skip freely (production already proves a `skipped` non-required context — e.g. `build` on every # PR — does not block merge). diff --git a/scripts/pr-changed-files.sh b/scripts/pr-changed-files.sh index 549ae27e5..0f0b84280 100755 --- a/scripts/pr-changed-files.sh +++ b/scripts/pr-changed-files.sh @@ -41,7 +41,8 @@ # This NARROWS the window, it does not erase it. The base is re-read after the paging round trips # alongside the head, so a retarget that is still in effect at that point fails closed; a retarget # that opens and closes strictly between the files call and the re-read is not observable from here. -# Pinning the diff to two shas would close it, and Gitea 1.25.4 cannot serve that: `compare/{base}... +# Pinning the diff to two shas would close it, and Gitea cannot serve that (measured on 1.25.4, +# re-confirmed on 1.27.1 2026-08-28 ersatztv#747): `compare/{base}... # {head}` returns `total_commits`/`commits` and NO `files`, and a `--depth=1` fetch of the two shas # has no merge base, so a three-dot diff is impossible while a two-dot one over-reports every commit # `main` gained since the branch point (both measured, #698). The remainder is covered one level up @@ -190,7 +191,10 @@ while [ "$page" -le "$MAX_PAGES" ]; do # "renamed rows MUST carry previous_filename" requirement entirely. (An earlier version of this # comment claimed the source path would be "dropped", which is not the mechanism; a maintainer # who tested that claim would find it false and might conclude the check is redundant.) - # `modified` is accepted alongside `changed` deliberately: live Gitea 1.25.4 emits `changed`, but a + # `modified` is accepted alongside `changed` deliberately. Live Gitea emits `changed`; the full + # emitted vocabulary was re-derived on 1.27.1 (2026-08-28, ersatztv#747) over the file rows of the + # 200 most recently updated PRs — exactly `changed`, `added`, `renamed`, `deleted`, all four already + # in the list below. `modified` is retained because a # closed allow-list built from the wrong vocabulary is a worse failure than the hole it closes — it # would gate every genuine docs-only PR on any version that spells it differently. The property is # "reject values we do not recognise", not "enumerate one version exactly". diff --git a/scripts/tests/test_merge_consent_exemption.py b/scripts/tests/test_merge_consent_exemption.py index fdf209ee8..c7c11f237 100644 --- a/scripts/tests/test_merge_consent_exemption.py +++ b/scripts/tests/test_merge_consent_exemption.py @@ -476,9 +476,11 @@ def test_a_rename_disguised_by_an_unknown_status_is_rejected(hook, status): def test_gitea_real_status_values_are_accepted(hook): - """Positive control for the closed set. The real Gitea 1.25.4 value for an edit is `changed`, - NOT `modified` — a closed allow-list built from the wrong vocabulary would reject every real - docs-only PR, which is a far worse failure than the hole it closes.""" + """Positive control for the closed set. The real Gitea value for an edit is `changed`, NOT + `modified` — a closed allow-list built from the wrong vocabulary would reject every real + docs-only PR, which is a far worse failure than the hole it closes. Measured on 1.25.4 and + re-derived on 1.27.1 (2026-08-28, ersatztv#747) across a 200-PR corpus, which emits exactly + `changed`, `added`, `renamed`, `deleted`.""" hook.set_pages( [ {"filename": "docs/a.md", "status": "changed"},