docs(747): re-probe the Gitea CI claims on 1.27.1, and measure the two that were source-attested only
The instance moved 1.25.4 -> 1.27.1 during #743, which left a set of deliberately version-pinned CI claims dated rather than wrong. The population was re-derived from `git ls-files` instead of from the issue's own 9-key list, which found ~21 claim sites across workflows, scripts, tests, docs and records. Re-confirmed unchanged on 1.27.1: * an `if:`-skipped job still reports commit-status `skipped`, a distinct state * `compare/{base}...{head}` still serves no `files` * no agent-side cancel route (POST 404, and live swagger has no cancel path at all) * `review-verdict.yml`'s `branches: [main]` still suppresses the run off a non-main base Re-derived from a constructed corpus rather than a sample, because two arbitrary PRs showed only two of the values: the PR file-status vocabulary is changed/added/renamed/ deleted over 200 PRs. All four were already in the closed allow-list, so that set is now evidenced rather than partly presumed. Newly measured, replacing source-attestation. All three arms ran on throwaway scratch-base protection rules, so `main`'s rule was never touched (its updated_at is unchanged at 2026-08-05): * `block_admin_merge_override: false` lets an admin force_merge past an ABSENT required context (HTTP 200); flipping only that field to `true` refuses the identical call (HTTP 405) - the negative control that makes it attributable * an absent required context BLOCKS, so the merge gate does not fail open when review-verdict.yml never runs * `enable_bypass_allowlist` is NOT a better-targeted substitute: enabled with an empty username list, the admin force_merge still succeeded * trap: the PR API reports `mergeable: true` while that same merge is refused, because the field reflects conflict-freedom and ignores branch protection Left explicitly dated rather than silently refreshed, each with its reason: push-supersession auto-cancel is no longer observable on this repo (no workflow triggers on a non-main push, and main refuses direct pushes), and the scope-enum / reqRepoWriter / write:package-403 items need a scoped PAT rather than a read-only probe. review-verdict.yml carries three more 1.25.4-dated comments and is deliberately untouched here: #763 is in-progress on that file and its paging work overlaps them. Refs: #747 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+18
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user