diff --git a/.gitea/workflows/checks.yml b/.gitea/workflows/checks.yml index bbaeea7..9bfbb15 100644 --- a/.gitea/workflows/checks.yml +++ b/.gitea/workflows/checks.yml @@ -3,12 +3,35 @@ # of real coverage including a wall-clock cadence test, and not one machine ever # ran them. Every merge here was a human reading a diff. # -# ⚠️ NO `paths:` FILTER, AND ITS ABSENCE IS LOAD-BEARING. Branch protection on -# release/1.0 requires the context `checks / unit (pull_request)`. Gitea leaves a -# required context it never receives PENDING forever, so the moment this workflow -# learns to sit a PR out, that PR becomes unmergeable. Condition a STEP if you +# ⚠️ NO EVENT FILTER OF ANY KIND - NO `paths:`, AND SINCE CI-9 NO `branches:` +# EITHER - AND THAT ABSENCE IS LOAD-BEARING. Branch protection on release/1.0 +# requires the context `checks / unit (pull_request)`. Condition a STEP if you # must; never the workflow. # +# ⚠️ CI-01 WROTE THE REASON DOWN WRONG, AND THE WRONG REASON WAS THE SAFE- +# SOUNDING ONE. It said an unreceived required context sits PENDING forever, so +# a sat-out pull request becomes unmergeable - annoying, but fail-safe. That is +# not what this Gitea does. MEASURED on Gitea 1.27.3, 2026-09-19, throwaway +# PR #94: when a workflow exists but its event filter does not match, Gitea does +# not stay silent. It POSTS the required context itself, with state `skipped`: +# +# GET /api/v1/repos/admin/goodwe-addon/commits//statuses +# [{"status":"skipped","context":"checks / unit (pull_request)", +# "description":"Skipped","target_url":""}] <- no run, no job, no url +# +# GET /api/v1/repos/admin/goodwe-addon/commits//status +# {"state":"success","total_count":1} <- the rollup LAUNDERS it +# +# and Gitea's merge gate accepts it. Same PR, same head sha, one status on it +# and that status a skip, POST .../pulls/94/merge answered 409 `head out of +# date` - the 409 being a deliberately wrong `head_commit_id` in the probe, i.e. +# the request had ALREADY CLEARED BRANCH PROTECTION. The control, a required +# context with no status at all, answered 405 `Not all required status checks +# successful`. So the true failure mode is the exact inverse of the one written +# here before: sitting a pull request out does not make it unmergeable, it makes +# it MERGEABLE WITHOUT EVER BEING TESTED, and the badge says success while it +# happens. Fail-open, not fail-closed. Do not reintroduce a filter here. +# # ⚠️ THE RUNNER IS SHARED WITH THE OTHER REPO'S RIG. `goodwe-runner` on # 192.168.2.147 carries the labels `self-hosted` and `goodwe-rig` and is # registered `:host`, and it is the only runner there is - so this job wears the @@ -23,9 +46,56 @@ # that step; a green from this workflow is a claim about content that can land. name: checks +# ⚠️ CI-9: `pull_request:` TAKES NO `branches:` LIST, ON PURPOSE. It used to +# read `branches: [release/1.0, master]`, which meant a STACKED pull request - +# one whose base is another feature branch rather than a release branch - did +# not match, and so was skipped, laundered to `success`, and mergeable untested +# by the mechanism described at the top of this file. +# +# ⚠️ AND RETARGETING DID NOT REPAIR IT, WHICH IS WHAT MADE THIS P1. The skip is +# a COMMIT STATUS ON THE HEAD SHA, not a property of the pull request, so moving +# the base afterwards leaves it exactly where it was. Observed live on PR #93 +# (2026-09-19): retargeted TEL-10 -> release/1.0, and the 10:05:00 `skipped` +# stayed attached to head a6af332 with the rollup still reading success. It was +# caught by a human who forced a real execution by reopening the pull request +# (run 799, 10:56). Reproduced deliberately on throwaway PR #94: retargeted onto +# a branch carrying a byte-identical copy of release/1.0's protection rule, and +# the single `skipped` status survived the retarget unchanged - no new run, no +# new status - and still cleared the gate. +# +# ⚠️ WHY NOT FIX THIS AT THE GATE INSTEAD, which would be the better place. It +# cannot be done. Gitea's branch protection cannot express "this context must be +# SUCCESS" as opposed to "this context must be PRESENT AND NOT FAILING". The +# whole of its status-check vocabulary is two fields - `enable_status_check` +# (bool) and `status_check_contexts` (a list of globs MATCHING CONTEXT NAMES) - +# confirmed against this server's own schema at /swagger.v1.json, definitions +# EditBranchProtectionOption and CreateBranchProtectionOption. There is no field +# for the required STATE anywhere in either. The success/skip comparison lives +# in Gitea's compiled code, so the only lever left is upstream of it: make sure +# the context is never a skip in the first place. Hence this block. +# +# ⚠️ WHAT THIS COSTS, stated honestly, because it is a real trade. Every pull +# request now runs the full suite, including the intermediate steps of a stack +# that nobody intends to merge on its own. A run is ~8 minutes wall clock +# (run 799: 7m55s) on a runner that is `capacity: 1` and shared with the other +# repo's rig, so each extra run is ~8 minutes of slot that a rig campaign then +# queues behind. Measured scale of the new work: of the 50 pull requests in this +# repository's history, 49 were opened directly against release/1.0 and would +# have run anyway; stacking is rare here but not hypothetical - PR #93 was +# stacked on TEL-10 five days ago and is the reason this file is being edited. +# The bill is therefore small today and grows with however much stacking the +# team adopts. It is worth paying: the alternative is that the cheap case stays +# untested and indistinguishable from the tested one. +# +# ⚠️ THIS DOES NOT CLOSE THE `skipped` HOLE, IT REMOVES THIS REPOSITORY'S ONLY +# CAUSE OF ONE. A skip from any other cause would still launder to success and +# still clear the gate, because that half is Gitea's and not ours. The one other +# cause that exists on this server is a skip-ci keyword in a commit message +# (`[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`, `[actions skip]`); +# see the CI-9 pull request body for the measurement and for the app.ini setting +# that disarms it, which is the forge owner's to apply, not this file's. on: pull_request: - branches: [release/1.0, master] workflow_dispatch: # ⚠️ NO `concurrency:` BLOCK, AND ITS ABSENCE IS AS LOAD-BEARING AS THE MISSING diff --git a/CI9-PROBE.txt b/CI9-PROBE.txt new file mode 100644 index 0000000..e2b70ae --- /dev/null +++ b/CI9-PROBE.txt @@ -0,0 +1,3 @@ +CI-9 throwaway probe base. Delete me. +head change +residual-hole probe