Two claims a green from checks / unit (pull_request) was making without ever having observed them. Both were measured on a deliberately conflicted throwaway pull request (#29, now closed, branch deleted) rather than read off the workflow — the Actions log endpoints 404 on this Gitea, so a step's own conclusion is the only channel out of a job, and each question below was asked as its own step.
Case 1 — and the card's mechanism is wrong in a way that makes it worse
The card says a conflicted PR loses its merge ref, so git fetch $GITHUB_REF || git fetch $GITHUB_SHA falls through and quietly grades the bare head. The symptom is real (PR #14: success while mergeable: false). The mechanism is not. Measured on the conflicted probe, runs 558/559:
probe step
result
GITHUB_REF == refs/pull/<n>/head
passed
GITHUB_REF == refs/pull/<n>/merge
failed
GITHUB_SHA == the PR head sha
passed
fetching GITHUB_REF succeeds (while conflicted)
passed
…and it lands on the bare head
passed
refs/pull/<n>/merge exists
failed
git ls-remote origin 'refs/pull/*' advertises only /head, for open and closed PRs alike. There are no merge refs on this server. So the first fetch has always succeeded, the fallback has never fired once, and both halves name the same commit anyway.
The gate did not lose merged content on conflicted PRs — it has never had merged content, on any PR in this repository's history. Green has meant "this branch passed on its own". block_on_outdated_branch is off on release/1.0, so the base has usually moved underneath it.
Case 2 — cancel-in-progress was never the knob
Runs 531 and 533 on PR #14 were cancelled at 0s, each at the exact second the next run on that PR was created:
531 created 17:08:19 "Waiting to run" cancelled 17:10:44 = 532's creation
533 created 17:45:11 "Waiting to run" cancelled 18:11:22 = 534's creation
Both have started_at at the Unix epoch — they never executed. A cancelled run posts failure on the required context, described Canceled after 0s. Branch protection compares state, not description, so "nothing ran" and "your tests are broken" arrive at the gate as the same red, wanting opposite operator responses.
Evidence — verified by producing it
AC 1, before and after on one PR, same conflict, only the gate swapped:
run
gate
PR mergeable
required context
567
the original checks.yml, unchanged
false
success — "Successful in 3m38s"
566
the checks.yml in this PR
false
failure in 2s
Run 566's per-step record is the other half of the point:
success check out the base branch and the pull request head
failure grade the merge, not the branch - refuse a PR that does not apply
skipped build the test image
skipped the add-on's own tests
success clean up
A reader of /actions/runs/566/jobs can see that no test ran. That distinction is why the refusal is its own named step.
AC 4 — established, not guessed. Three concurrency settings, each with one run holding the single runner slot and two further pushes behind it:
trial
setting
outcome
1
noconcurrency: block
runs 561 and 562 both sat queued together, both executed
2
CI-01's group, cancel-in-progress: **false**
run 564 sat Blocked by required conditions and was cancelled at 0s the second run 565 was created
A shared group cancels a run that has not started regardless of that flag — GitHub documents it, Gitea 1.27 implements it. Having no group is the only configuration measured to leave a queued run alive, so the block is gone and the measurement is written where the block was.
A sha-keyed group was considered and rejected: the only cancellations it still permits are same-sha ones, and those are the dangerous half — their red lands on a commit that is still the head.
AC 3. A commit that never had a run carries an empty /statuses/<sha> list; a queued one carries pending / Waiting to run within seconds; a concurrency-held one carries pending / Blocked by required conditions. Those were already distinguishable at the API, and a superseded commit's status stays on that commit — 0942228d read failure while d31a12b7 read success, per-commit. /actions/tasks, which is what was polled when this was first reported, lists only tasks that started and cannot answer the question at all.
What changed
.gitea/workflows/checks.yml — the checkout fetches the base tip and the PR head and merges them; a PR that does not apply is refused in a separate named step that says NOTHING HAS BEEN TESTED. --depth 1 is gone (a merge needs a merge base; a full fetch here measured 6.8 MB / 1.1 s). The || fallback survives only on the workflow_dispatch path — which has no PR to merge — and now announces which bare ref it took. concurrency: removed, with the measurement above written in its place.
goodwe_controller/test_ci_gate.py — grades the gate by running it: extracts the checkout and merge steps out of checks.yml and executes the shipped commands against two throwaway local repos it builds, one whose PR merges and one whose does not. Part 3 reproduces the defect (the old one-liner exits 0 on the unmergeable one). Part 1 pins the two tokens that assemble checks / unit (pull_request).
.gitea/ci/Dockerfile — git, for that test. Local fixtures, no network.
name: checks and the unit: job key are untouched — verified by parsing the file, and part 1 of the new test is now a permanent tripwire on exactly that: renaming either leaves every open and future PR waiting forever on a context that never arrives.
Verified by breaking it. Seven mutations of checks.yml, each confirmed red: rename the workflow, rename the job key, restore the old one-liner, restore the concurrency block, stop refusing, drop the announcement, put --depth 1 back. 17/17 green in the checks image.
What I am not sure about, and what this does not fix
This does not make a cancelled run distinguishable at the gate by state. Gitea posts that status, not this workflow, and no edit here can change it. What ships removes the cause rather than relabelling the symptom. If a cancellation ever happens by another route, the gate will still show a red that means "nothing ran".
The cost of removing concurrency: is real and is borne by a shared, capacity-1 runner. A developer who pushes four times now occupies it four times. I judged that CI-01's own trade written the other way round — "a delayed verdict, never a destroyed one" — but it is a judgement, and if the rig starts starving, the honest lever is runner capacity, not putting the group back.
Grading the merge changes what a red means. A PR can now go red because the base moved, not because the branch is wrong. That is the correct verdict about landing content, and block_on_outdated_branch is off precisely so people are not forced to rebase — so expect the occasional "it passed yesterday" that is a true statement about a different base.
The workflow_dispatch fallback is kept on a case nobody has ruled out (a branch deleted or renamed between dispatch and fetch), not on one I reproduced. It is announced rather than silent, which is what AC 2 asks for, but I did not produce the situation it exists for.
.gitea/ci/Dockerfile is now graded from the merged tree, like everything else — a PR cannot grade itself with an image the base has not seen. I believe that is the right way round; it does mean a Dockerfile change is tested as merged, not as written.
Closes CI-02.
## What was wrong
Two claims a green from `checks / unit (pull_request)` was making without ever having observed them. Both were measured on a deliberately conflicted throwaway pull request (#29, now closed, branch deleted) rather than read off the workflow — the Actions log endpoints 404 on this Gitea, so a step's own conclusion is the only channel out of a job, and each question below was asked as its own step.
### Case 1 — and the card's mechanism is wrong in a way that makes it worse
The card says a conflicted PR loses its merge ref, so `git fetch $GITHUB_REF || git fetch $GITHUB_SHA` falls through and quietly grades the bare head. The **symptom** is real (PR #14: `success` while `mergeable: false`). The **mechanism** is not. Measured on the conflicted probe, runs 558/559:
| probe step | result |
|---|---|
| `GITHUB_REF == refs/pull/<n>/head` | **passed** |
| `GITHUB_REF == refs/pull/<n>/merge` | failed |
| `GITHUB_SHA == the PR head sha` | **passed** |
| fetching `GITHUB_REF` succeeds (while conflicted) | **passed** |
| …and it lands on the bare head | **passed** |
| `refs/pull/<n>/merge` exists | failed |
`git ls-remote origin 'refs/pull/*'` advertises only `/head`, for open and closed PRs alike. **There are no merge refs on this server.** So the first fetch has always succeeded, **the fallback has never fired once**, and both halves name the same commit anyway.
The gate did not *lose* merged content on conflicted PRs — it has **never had** merged content, on any PR in this repository's history. Green has meant "this branch passed on its own". `block_on_outdated_branch` is off on release/1.0, so the base has usually moved underneath it.
### Case 2 — `cancel-in-progress` was never the knob
Runs 531 and 533 on PR #14 were cancelled at 0s, each at the exact second the next run on that PR was created:
```
531 created 17:08:19 "Waiting to run" cancelled 17:10:44 = 532's creation
533 created 17:45:11 "Waiting to run" cancelled 18:11:22 = 534's creation
```
Both have `started_at` at the Unix epoch — they never executed. A cancelled run posts `failure` on the required context, described `Canceled after 0s`. Branch protection compares **state**, not description, so *"nothing ran"* and *"your tests are broken"* arrive at the gate as the same red, wanting opposite operator responses.
## Evidence — verified by producing it
**AC 1, before and after on one PR, same conflict, only the gate swapped:**
| run | gate | PR `mergeable` | required context |
|---|---|---|---|
| 567 | the **original** checks.yml, unchanged | `false` | **`success`** — "Successful in 3m38s" |
| 566 | the checks.yml in this PR | `false` | **`failure`** in 2s |
Run 566's per-step record is the other half of the point:
```
success check out the base branch and the pull request head
failure grade the merge, not the branch - refuse a PR that does not apply
skipped build the test image
skipped the add-on's own tests
success clean up
```
A reader of `/actions/runs/566/jobs` can see that **no test ran**. That distinction is why the refusal is its own named step.
**AC 4 — established, not guessed.** Three concurrency settings, each with one run holding the single runner slot and two further pushes behind it:
| trial | setting | outcome |
|---|---|---|
| 1 | **no** `concurrency:` block | runs 561 and 562 both sat queued together, **both executed** |
| 2 | CI-01's group, `cancel-in-progress: **false**` | run 564 sat `Blocked by required conditions` and was **cancelled at 0s** the second run 565 was created |
A shared group cancels a run that has not started **regardless of that flag** — GitHub documents it, Gitea 1.27 implements it. Having no group is the only configuration measured to leave a queued run alive, so the block is gone and the measurement is written where the block was.
A sha-keyed group was considered and rejected: the only cancellations it still permits are same-sha ones, and those are the dangerous half — their red lands on a commit that **is** still the head.
**AC 3.** A commit that never had a run carries an empty `/statuses/<sha>` list; a queued one carries `pending / Waiting to run` within seconds; a concurrency-held one carries `pending / Blocked by required conditions`. Those were already distinguishable at the API, and a superseded commit's status stays on that commit — `0942228d` read `failure` while `d31a12b7` read `success`, per-commit. `/actions/tasks`, which is what was polled when this was first reported, lists only tasks that **started** and cannot answer the question at all.
## What changed
- **`.gitea/workflows/checks.yml`** — the checkout fetches the base tip and the PR head and **merges** them; a PR that does not apply is **refused** in a separate named step that says `NOTHING HAS BEEN TESTED`. `--depth 1` is gone (a merge needs a merge base; a full fetch here measured 6.8 MB / 1.1 s). The `||` fallback survives only on the `workflow_dispatch` path — which has no PR to merge — and now announces which bare ref it took. `concurrency:` removed, with the measurement above written in its place.
- **`goodwe_controller/test_ci_gate.py`** — grades the gate by **running** it: extracts the checkout and merge steps out of checks.yml and executes the shipped commands against two throwaway local repos it builds, one whose PR merges and one whose does not. Part 3 reproduces the defect (the old one-liner exits 0 on the unmergeable one). Part 1 pins the two tokens that assemble `checks / unit (pull_request)`.
- **`.gitea/ci/Dockerfile`** — `git`, for that test. Local fixtures, no network.
**`name: checks` and the `unit:` job key are untouched** — verified by parsing the file, and part 1 of the new test is now a permanent tripwire on exactly that: renaming either leaves every open and future PR waiting forever on a context that never arrives.
**Verified by breaking it.** Seven mutations of checks.yml, each confirmed red: rename the workflow, rename the job key, restore the old one-liner, restore the concurrency block, stop refusing, drop the announcement, put `--depth 1` back. 17/17 green in the checks image.
## What I am not sure about, and what this does not fix
1. **This does not make a cancelled run distinguishable at the gate by state.** Gitea posts that status, not this workflow, and no edit here can change it. What ships removes the *cause* rather than relabelling the symptom. If a cancellation ever happens by another route, the gate will still show a red that means "nothing ran".
2. **The cost of removing `concurrency:` is real and is borne by a shared, capacity-1 runner.** A developer who pushes four times now occupies it four times. I judged that CI-01's own trade written the other way round — "a delayed verdict, never a destroyed one" — but it is a judgement, and if the rig starts starving, the honest lever is runner capacity, not putting the group back.
3. **Grading the merge changes what a red means.** A PR can now go red because the *base* moved, not because the branch is wrong. That is the correct verdict about landing content, and `block_on_outdated_branch` is off precisely so people are not forced to rebase — so expect the occasional "it passed yesterday" that is a true statement about a different base.
4. **The `workflow_dispatch` fallback is kept on a case nobody has ruled out** (a branch deleted or renamed between dispatch and fetch), not on one I reproduced. It is announced rather than silent, which is what AC 2 asks for, but I did not produce the situation it exists for.
5. **`.gitea/ci/Dockerfile` is now graded from the merged tree**, like everything else — a PR cannot grade itself with an image the base has not seen. I believe that is the right way round; it does mean a Dockerfile change is tested as merged, not as written.
Closes CI-02.
Two things a green from `checks / unit (pull_request)` was claiming without
having observed. Both were measured on a deliberately conflicted throwaway pull
request (#29, now closed and its branch deleted) rather than read off the
workflow, because the Actions log endpoints 404 on this Gitea and a step's own
conclusion is the only channel out of a job.
CASE 1, AND THE TICKET'S MECHANISM IS WRONG IN A WAY THAT MAKES IT WORSE.
The card says a conflicted pull request loses its merge ref, so that
git fetch -q --depth 1 origin "$GITHUB_REF" \
|| git fetch -q --depth 1 origin "$GITHUB_SHA"
falls through to the second half and quietly grades the bare branch head. The
symptom is real - PR #14 reported success while `mergeable: false` - but that
is not how. Asking one question per step on the conflicted probe (runs 558,
559):
GITHUB_REF == refs/pull/<n>/head P01 passed
GITHUB_REF != refs/pull/<n>/merge P02 failed
GITHUB_SHA == the pull request head sha P03 passed
fetching GITHUB_REF succeeds P04 passed <- even conflicted
...and it lands on the bare head P05 passed
refs/pull/<n>/merge does not exist P06 failed
`git ls-remote origin 'refs/pull/*'` on this server advertises only `/head`, for
open and closed pull requests alike. There are no merge refs here at all. The
first fetch has therefore always succeeded and the fallback has never fired
once; both halves name the same commit anyway. So the gate did not lose merged
content on conflicted pull requests - it has never had merged content, on any
pull request in this repository's history. Green has meant "this branch passed
on its own", and with `block_on_outdated_branch` off on release/1.0 the base has
usually moved underneath it.
Reproduced end to end on one pull request, same conflict, gate swapped:
run 567 the ORIGINAL gate -> success, "Successful in 3m38s"
run 566 the gate below -> failure in 2s, merge step red,
both test steps `skipped`
The checkout now fetches the base tip and the pull request head, merges them,
and refuses if the merge does not apply - as its own NAMED step, because
/actions/runs/<id>/jobs carries a per-step conclusion and that is what lets a
reader tell "nothing was tested" from "the tests spoke". `--depth 1` is gone: a
merge needs a merge base, and a full fetch of this repository measured 6.8 MB
and 1.1 s. The fallback survives only on the workflow_dispatch path, which has
no pull request to merge and now announces which bare ref it took.
CASE 2, AND `cancel-in-progress` IS NOT THE KNOB.
Runs 531 and 533 on PR #14 were cancelled at 0s, each at the exact second the
next run on that pull request was created:
531 created 17:08:19 `Waiting to run`, cancelled 17:10:44 = 532's creation
533 created 17:45:11 `Waiting to run`, cancelled 18:11:22 = 534's creation
Both have `started_at` at the Unix epoch. A cancelled run posts `failure` on the
required context, described `Canceled after 0s`; branch protection compares
state, not description, so "nothing ran" and "your tests are broken" arrive as
the same red wanting opposite responses. Three settings were measured, each with
one run holding the single runner slot and two further pushes behind it:
no concurrency block at all runs 561 and 562 both queued, both executed
group, cancel-in-progress:false
run 564 sat `Blocked by required conditions`
and was cancelled at 0s the second 565 existed
A shared group cancels a run that has not started regardless of that flag -
GitHub documents it and Gitea 1.27 implements it. Having no group is the only
configuration measured to leave a queued run alive, so the block is gone and the
measurement is written above it. The cost is stated in the file: a developer who
pushes four times occupies the shared runner four times. That is CI-01's own
trade written the other way round - a delayed verdict, never a destroyed one -
and a destroyed verdict is worse here because it is indistinguishable from a
real failure. A sha-keyed group was considered and rejected: the only
cancellations it still permits are same-sha ones, whose red lands on a commit
that IS still the head.
WHAT IS NOT ESTABLISHED, SAID PLAINLY. Nothing here makes a cancelled run
distinguishable at the gate BY STATE; that is Gitea posting the status, not this
workflow, and no edit to this file can change it. What ships removes the cause
instead of relabelling the symptom. And a commit that never had a run carries an
empty `/statuses/<sha>` list, while a queued one carries `pending / Waiting to
run` within seconds and a concurrency-held one `pending / Blocked by required
conditions` - those were already distinguishable at the API. `/actions/tasks`,
which is what was polled when this was first reported, lists only tasks that
started and cannot answer the question at all.
test_ci_gate.py grades the gate by RUNNING it. It extracts the checkout and
merge steps out of checks.yml and executes the shipped commands against two
throwaway local repositories it builds - one whose pull request merges, one
whose does not - and pins that the old one-liner exits 0 on the unmergeable one
while the new steps refuse and say nothing was tested. Part 1 pins the two
tokens that assemble `checks / unit (pull_request)`: renaming `name:` or the
`unit:` job key would leave every open and future pull request waiting forever
on a context that never arrives, with no error anywhere in that sequence, and
admin override blocked. Seven mutations of checks.yml were each confirmed red,
including both renames and a restored `--depth 1`. .gitea/ci/Dockerfile gains
`git` for it; the fixtures are local paths and no network is touched.
17/17 green in the checks image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
admin
merged commit 99321892cb into release/1.02026-09-01 04:06:27 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What was wrong
Two claims a green from
checks / unit (pull_request)was making without ever having observed them. Both were measured on a deliberately conflicted throwaway pull request (#29, now closed, branch deleted) rather than read off the workflow — the Actions log endpoints 404 on this Gitea, so a step's own conclusion is the only channel out of a job, and each question below was asked as its own step.Case 1 — and the card's mechanism is wrong in a way that makes it worse
The card says a conflicted PR loses its merge ref, so
git fetch $GITHUB_REF || git fetch $GITHUB_SHAfalls through and quietly grades the bare head. The symptom is real (PR #14:successwhilemergeable: false). The mechanism is not. Measured on the conflicted probe, runs 558/559:GITHUB_REF == refs/pull/<n>/headGITHUB_REF == refs/pull/<n>/mergeGITHUB_SHA == the PR head shaGITHUB_REFsucceeds (while conflicted)refs/pull/<n>/mergeexistsgit ls-remote origin 'refs/pull/*'advertises only/head, for open and closed PRs alike. There are no merge refs on this server. So the first fetch has always succeeded, the fallback has never fired once, and both halves name the same commit anyway.The gate did not lose merged content on conflicted PRs — it has never had merged content, on any PR in this repository's history. Green has meant "this branch passed on its own".
block_on_outdated_branchis off on release/1.0, so the base has usually moved underneath it.Case 2 —
cancel-in-progresswas never the knobRuns 531 and 533 on PR #14 were cancelled at 0s, each at the exact second the next run on that PR was created:
Both have
started_atat the Unix epoch — they never executed. A cancelled run postsfailureon the required context, describedCanceled after 0s. Branch protection compares state, not description, so "nothing ran" and "your tests are broken" arrive at the gate as the same red, wanting opposite operator responses.Evidence — verified by producing it
AC 1, before and after on one PR, same conflict, only the gate swapped:
mergeablefalsesuccess— "Successful in 3m38s"falsefailurein 2sRun 566's per-step record is the other half of the point:
A reader of
/actions/runs/566/jobscan see that no test ran. That distinction is why the refusal is its own named step.AC 4 — established, not guessed. Three concurrency settings, each with one run holding the single runner slot and two further pushes behind it:
concurrency:blockcancel-in-progress: **false**Blocked by required conditionsand was cancelled at 0s the second run 565 was createdA shared group cancels a run that has not started regardless of that flag — GitHub documents it, Gitea 1.27 implements it. Having no group is the only configuration measured to leave a queued run alive, so the block is gone and the measurement is written where the block was.
A sha-keyed group was considered and rejected: the only cancellations it still permits are same-sha ones, and those are the dangerous half — their red lands on a commit that is still the head.
AC 3. A commit that never had a run carries an empty
/statuses/<sha>list; a queued one carriespending / Waiting to runwithin seconds; a concurrency-held one carriespending / Blocked by required conditions. Those were already distinguishable at the API, and a superseded commit's status stays on that commit —0942228dreadfailurewhiled31a12b7readsuccess, per-commit./actions/tasks, which is what was polled when this was first reported, lists only tasks that started and cannot answer the question at all.What changed
.gitea/workflows/checks.yml— the checkout fetches the base tip and the PR head and merges them; a PR that does not apply is refused in a separate named step that saysNOTHING HAS BEEN TESTED.--depth 1is gone (a merge needs a merge base; a full fetch here measured 6.8 MB / 1.1 s). The||fallback survives only on theworkflow_dispatchpath — which has no PR to merge — and now announces which bare ref it took.concurrency:removed, with the measurement above written in its place.goodwe_controller/test_ci_gate.py— grades the gate by running it: extracts the checkout and merge steps out of checks.yml and executes the shipped commands against two throwaway local repos it builds, one whose PR merges and one whose does not. Part 3 reproduces the defect (the old one-liner exits 0 on the unmergeable one). Part 1 pins the two tokens that assemblechecks / unit (pull_request)..gitea/ci/Dockerfile—git, for that test. Local fixtures, no network.name: checksand theunit:job key are untouched — verified by parsing the file, and part 1 of the new test is now a permanent tripwire on exactly that: renaming either leaves every open and future PR waiting forever on a context that never arrives.Verified by breaking it. Seven mutations of checks.yml, each confirmed red: rename the workflow, rename the job key, restore the old one-liner, restore the concurrency block, stop refusing, drop the announcement, put
--depth 1back. 17/17 green in the checks image.What I am not sure about, and what this does not fix
concurrency:is real and is borne by a shared, capacity-1 runner. A developer who pushes four times now occupies it four times. I judged that CI-01's own trade written the other way round — "a delayed verdict, never a destroyed one" — but it is a judgement, and if the rig starts starving, the honest lever is runner capacity, not putting the group back.block_on_outdated_branchis off precisely so people are not forced to rebase — so expect the occasional "it passed yesterday" that is a true statement about a different base.workflow_dispatchfallback is kept on a case nobody has ruled out (a branch deleted or renamed between dispatch and fetch), not on one I reproduced. It is announced rather than silent, which is what AC 2 asks for, but I did not produce the situation it exists for..gitea/ci/Dockerfileis now graded from the merged tree, like everything else — a PR cannot grade itself with an image the base has not seen. I believe that is the right way round; it does mean a Dockerfile change is tested as merged, not as written.Closes CI-02.