Refs #416 (not fixes — the "verified on a real docs-only PR" Done-when box can only be ticked after this lands, since the gate takes effect on main; see below).
What
A docs-only change (docs/** or *.md) ran the entire docker-build.yml matrix — test, migrations (incl. its mysql:8.4 service), functional-e2e, format, api-docs — ~9 min of warm CI to validate Markdown. This skips the heavy work without bricking the merge gate.
The trap, and why this shape
main's branch protection requires two contexts by name (Build & test (.NET), EF migration integrity (SQLite + MySql)). A workflow-level paths-ignore — or an if:-skipped required job — would produce no run for those contexts, so a docs-only PR could never merge. The naive fix bricks docs PRs.
I answered the issue's central open question empirically with a throwaway probe (now-closed PR #418): on Gitea 1.25.4 an if:-skipped job reports commit-status state skipped (a distinct state, not success). So this change does notif:-skip the required jobs.
How
New scripts/ci-detect-docs-only.sh emits docs_only=true|false. Each heavy job (test, migrations, functional-e2e, build) runs it as its first post-checkout step (id: detect) and gates every real step on docs_only != 'true'. The jobs always run and report success in seconds on docs-only → both required contexts keep reporting → docs-only PRs stay mergeable by construction. This is the same id: detect → step-if: pattern api-docs/format already use in production.
Non-required jobs may skip freely (production proves a skipped non-required context — build on every PR — doesn't block merge), so build also skips its image steps on a docs-only push to main.
Tag builds force docs_only=false — a release is never skipped.
Detection biases toward running more: docs_only=true only when every changed path is docs/** or *.md; any code path, tag, non-merge push, or undeterminable diff → full matrix. Uses --no-renames so a code→docs rename can't be misclassified (review finding).
migrations' mysql service still starts on a docs-only run (a services: container starts with the job regardless of step if:), but the 787-migration replay — the expensive part — is skipped.
Verification done
Local: yaml parse, shellcheck clean, an 8-case detection test matrix (PR/push/tag/dispatch; docs-only vs code vs web vs workflow-yml vs code→docs-rename), decisions append-only guard.
Cold adversarial review: MERGEABLE; its one MEDIUM (rename misclassification) is fixed here via --no-renames.
Still to verify post-merge (why this is Refs, not fixes)
The gate only takes effect once on main. After merge I'll open (a) a docs-only PR and confirm both required contexts report green in seconds and it's mergeable, and (b) a .cs-only PR confirming the full matrix still runs — then tick the remaining Done-when boxes and close#416.
Docs
docs/ci-cd.md → new "Docs-only skip" section + triggers-table note; docs/decisions.md entry. The separate PR-vs-main-rerun redundancy your comment raised is filed as #420 (distinct from the within-run triple build, #398).
Refs #416 (not `fixes` — the "verified on a real docs-only PR" Done-when box can only be ticked *after* this lands, since the gate takes effect on `main`; see below).
## What
A docs-only change (`docs/**` or `*.md`) ran the entire `docker-build.yml` matrix — `test`, `migrations` (incl. its `mysql:8.4` service), `functional-e2e`, `format`, `api-docs` — ~9 min of warm CI to validate Markdown. This skips the heavy work **without** bricking the merge gate.
## The trap, and why this shape
`main`'s branch protection requires two contexts **by name** (`Build & test (.NET)`, `EF migration integrity (SQLite + MySql)`). A workflow-level `paths-ignore` — or an `if:`-skipped required job — would produce **no run** for those contexts, so a docs-only PR could **never merge**. The naive fix bricks docs PRs.
I answered the issue's central open question empirically with a throwaway probe (now-closed PR #418): on **Gitea 1.25.4** an `if:`-skipped job reports commit-status state **`skipped`** (a distinct state, not `success`). So this change does **not** `if:`-skip the required jobs.
## How
New `scripts/ci-detect-docs-only.sh` emits `docs_only=true|false`. Each heavy job (`test`, `migrations`, `functional-e2e`, `build`) runs it as its first post-checkout step (`id: detect`) and gates **every real step** on `docs_only != 'true'`. The jobs **always run and report `success` in seconds** on docs-only → both required contexts keep reporting → docs-only PRs stay mergeable *by construction*. This is the same `id: detect` → step-`if:` pattern `api-docs`/`format` already use in production.
- Non-required jobs may skip freely (production proves a `skipped` non-required context — `build` on every PR — doesn't block merge), so `build` also skips its image steps on a **docs-only push to `main`**.
- **Tag builds force `docs_only=false`** — a release is never skipped.
- Detection **biases toward running more**: `docs_only=true` only when *every* changed path is `docs/**` or `*.md`; any code path, tag, non-merge push, or undeterminable diff → full matrix. Uses `--no-renames` so a code→docs rename can't be misclassified (review finding).
- `migrations`' `mysql` service still starts on a docs-only run (a `services:` container starts with the job regardless of step `if:`), but the 787-migration replay — the expensive part — is skipped.
## Verification done
- Local: yaml parse, shellcheck clean, an 8-case detection test matrix (PR/push/tag/dispatch; docs-only vs code vs web vs workflow-yml vs code→docs-rename), decisions append-only guard.
- Cold adversarial review: **MERGEABLE**; its one MEDIUM (rename misclassification) is fixed here via `--no-renames`.
## Still to verify post-merge (why this is `Refs`, not `fixes`)
The gate only takes effect once on `main`. After merge I'll open (a) a docs-only PR and confirm both required contexts report green in seconds and it's mergeable, and (b) a `.cs`-only PR confirming the full matrix still runs — then tick the remaining Done-when boxes and close #416.
## Docs
`docs/ci-cd.md` → new "Docs-only skip" section + triggers-table note; `docs/decisions.md` entry. The separate PR-vs-main-rerun redundancy your comment raised is filed as #420 (distinct from the within-run triple build, #398).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cold-context adversarial review (review-only agent, independent of implementation), then a re-review of the fix commit at the current head:
Merge-gate safety — confirmed safe by construction. Both required contexts (test, migrations) carry no job-level if:; they always run, gate only their steps, and report success in seconds on docs-only. Same id: detect → step-if: pattern api-docs/format already use in production.
Tag/release safety — a v* build can never be skipped (GITHUB_REF_TYPE=tag short-circuits to docs_only=false, same assumption the existing meta step relies on).
build meta outputs — every consumer of steps.meta.outputs.* is gated by the same condition, so nothing reads them while running.
One MEDIUM found and fixed: git diff --name-only has rename detection on by default, so a code→docs rename (Foo.cs→docs/Foo.md) would show only the destination and be misclassified as docs-only, skipping required tests on a code change. Fixed with --no-renames (empirically verified: the deletion Foo.cs now surfaces → full matrix). Re-review confirmed the fix is applied on the single changed-set git diff, closes the hole, and regresses nothing.
The only delta since the first review is that one-line hardening (folded into commit 1) plus the docs commit (no code).
Review-verdict: MERGEABLE @ 52b37614
Cold-context adversarial review (review-only agent, independent of implementation), then a re-review of the fix commit at the current head:
- **Merge-gate safety — confirmed safe by construction.** Both required contexts (`test`, `migrations`) carry no job-level `if:`; they always run, gate only their steps, and report `success` in seconds on docs-only. Same `id: detect` → step-`if:` pattern `api-docs`/`format` already use in production.
- **Tag/release safety** — a `v*` build can never be skipped (`GITHUB_REF_TYPE=tag` short-circuits to `docs_only=false`, same assumption the existing meta step relies on).
- **build meta outputs** — every consumer of `steps.meta.outputs.*` is gated by the same condition, so nothing reads them while running.
- **One MEDIUM found and fixed**: `git diff --name-only` has rename detection on by default, so a code→docs rename (`Foo.cs`→`docs/Foo.md`) would show only the destination and be misclassified as docs-only, skipping required tests on a code change. Fixed with `--no-renames` (empirically verified: the deletion `Foo.cs` now surfaces → full matrix). Re-review confirmed the fix is applied on the single changed-set `git diff`, closes the hole, and regresses nothing.
The only delta since the first review is that one-line hardening (folded into commit 1) plus the docs commit (no code).
Review-verdict: MERGEABLE @ 52b37614
⚠️Correction — the previous MERGEABLE @ 52b37614 was against a commit that did NOT contain the --no-renames fix. Process error on my side: the fix was applied in the working tree and empirically verified there, but never committed before the first push (index/worktree mismatch), so 52b37614 — and the CI that ran on it — still had the rename hole, and the re-review confirmed my working tree rather than the pushed commit.
Fixed properly now:
--no-renames is committed (verified on the pushed commit, not just the working tree — 3 occurrences in scripts/ci-detect-docs-only.sh).
Rebased onto current main (ffa8e5a0, past #419); diff is exactly the 4 intended files, no unrelated changes.
New head: f7b97adc. Superseded verdict @ 52b37614 is void; a fresh verdict @ f7b97adc follows once CI is green and the reviewer confirms the pushed commit.
⚠️ **Correction — the previous `MERGEABLE @ 52b37614` was against a commit that did NOT contain the `--no-renames` fix.** Process error on my side: the fix was applied in the working tree and empirically verified there, but never committed before the first push (index/worktree mismatch), so `52b37614` — and the CI that ran on it — still had the rename hole, and the re-review confirmed my working tree rather than the pushed commit.
Fixed properly now:
- `--no-renames` is committed (verified on the **pushed** commit, not just the working tree — 3 occurrences in `scripts/ci-detect-docs-only.sh`).
- Rebased onto current `main` (`ffa8e5a0`, past #419); diff is exactly the 4 intended files, no unrelated changes.
- New head: **`f7b97adc`**. Superseded verdict `@ 52b37614` is void; a fresh verdict `@ f7b97adc` follows once CI is green and the reviewer confirms the pushed commit.
Re-review of the **pushed commit** `f7b97adc` (verified against `git show f7b97adc:…`, not the working tree, given the earlier mismatch):
- Committed script carries the fix — line 80 `git diff --no-renames --name-only "$range"` is in the pushed object.
- Diff is exactly the 4 intended files; no #419 revert leakage.
- Step-gating unchanged (32 `docs_only != 'true'` guards); only material change vs. the reviewed content is the committed `--no-renames` hardening.
Review-verdict: MERGEABLE @ f7b97adc
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.
Refs #416 (not
fixes— the "verified on a real docs-only PR" Done-when box can only be ticked after this lands, since the gate takes effect onmain; see below).What
A docs-only change (
docs/**or*.md) ran the entiredocker-build.ymlmatrix —test,migrations(incl. itsmysql:8.4service),functional-e2e,format,api-docs— ~9 min of warm CI to validate Markdown. This skips the heavy work without bricking the merge gate.The trap, and why this shape
main's branch protection requires two contexts by name (Build & test (.NET),EF migration integrity (SQLite + MySql)). A workflow-levelpaths-ignore— or anif:-skipped required job — would produce no run for those contexts, so a docs-only PR could never merge. The naive fix bricks docs PRs.I answered the issue's central open question empirically with a throwaway probe (now-closed PR #418): on Gitea 1.25.4 an
if:-skipped job reports commit-status stateskipped(a distinct state, notsuccess). So this change does notif:-skip the required jobs.How
New
scripts/ci-detect-docs-only.shemitsdocs_only=true|false. Each heavy job (test,migrations,functional-e2e,build) runs it as its first post-checkout step (id: detect) and gates every real step ondocs_only != 'true'. The jobs always run and reportsuccessin seconds on docs-only → both required contexts keep reporting → docs-only PRs stay mergeable by construction. This is the sameid: detect→ step-if:patternapi-docs/formatalready use in production.skippednon-required context —buildon every PR — doesn't block merge), sobuildalso skips its image steps on a docs-only push tomain.docs_only=false— a release is never skipped.docs_only=trueonly when every changed path isdocs/**or*.md; any code path, tag, non-merge push, or undeterminable diff → full matrix. Uses--no-renamesso a code→docs rename can't be misclassified (review finding).migrations'mysqlservice still starts on a docs-only run (aservices:container starts with the job regardless of stepif:), but the 787-migration replay — the expensive part — is skipped.Verification done
--no-renames.Still to verify post-merge (why this is
Refs, notfixes)The gate only takes effect once on
main. After merge I'll open (a) a docs-only PR and confirm both required contexts report green in seconds and it's mergeable, and (b) a.cs-only PR confirming the full matrix still runs — then tick the remaining Done-when boxes and close #416.Docs
docs/ci-cd.md→ new "Docs-only skip" section + triggers-table note;docs/decisions.mdentry. The separate PR-vs-main-rerun redundancy your comment raised is filed as #420 (distinct from the within-run triple build, #398).🤖 Generated with Claude Code
Cold-context adversarial review (review-only agent, independent of implementation), then a re-review of the fix commit at the current head:
test,migrations) carry no job-levelif:; they always run, gate only their steps, and reportsuccessin seconds on docs-only. Sameid: detect→ step-if:patternapi-docs/formatalready use in production.v*build can never be skipped (GITHUB_REF_TYPE=tagshort-circuits todocs_only=false, same assumption the existing meta step relies on).steps.meta.outputs.*is gated by the same condition, so nothing reads them while running.git diff --name-onlyhas rename detection on by default, so a code→docs rename (Foo.cs→docs/Foo.md) would show only the destination and be misclassified as docs-only, skipping required tests on a code change. Fixed with--no-renames(empirically verified: the deletionFoo.csnow surfaces → full matrix). Re-review confirmed the fix is applied on the single changed-setgit diff, closes the hole, and regresses nothing.The only delta since the first review is that one-line hardening (folded into commit 1) plus the docs commit (no code).
Review-verdict: MERGEABLE @ 52b37614
52b37614cctof7b97adce8⚠️ Correction — the previous
MERGEABLE @ 52b37614was against a commit that did NOT contain the--no-renamesfix. Process error on my side: the fix was applied in the working tree and empirically verified there, but never committed before the first push (index/worktree mismatch), so52b37614— and the CI that ran on it — still had the rename hole, and the re-review confirmed my working tree rather than the pushed commit.Fixed properly now:
--no-renamesis committed (verified on the pushed commit, not just the working tree — 3 occurrences inscripts/ci-detect-docs-only.sh).main(ffa8e5a0, past #419); diff is exactly the 4 intended files, no unrelated changes.f7b97adc. Superseded verdict@ 52b37614is void; a fresh verdict@ f7b97adcfollows once CI is green and the reviewer confirms the pushed commit.Re-review of the pushed commit
f7b97adc(verified againstgit show f7b97adc:…, not the working tree, given the earlier mismatch):git diff --no-renames --name-only "$range"is in the pushed object.docs_only != 'true'guards); only material change vs. the reviewed content is the committed--no-renameshardening.Review-verdict: MERGEABLE @
f7b97adc