Count both paths of a rename in the docs-only CI skip; pause auto-release #55

Merged
lilleman merged 4 commits from skip-ci-for-root-markdown into main 2026-08-05 10:46:53 +02:00
4 changed files with 28 additions and 9 deletions
Showing only changes of commit 2fb5e695e1 - Show all commits
+17
View File
@@ -168,6 +168,23 @@ them. Revisit only if the stated reason stops holding.
workspace dir, so ci.sh's web-image build races another run's container creation on the
`<project>-web` tag. Accepted for a single-maintainer cadence; serialize with a workflow
`concurrency` group if it ever bites.
- **The docs-only CI skip is `*.md` anywhere in the tree, not just the root.** No test, build step or
workflow reads a markdown file (`README-dockerhub.md` is pasted into Docker Hub by hand), so a
nested `examples/plugins/admin/README.md` edit is as safe to skip as `README.md`, and narrowing it
would spend the full gate on one. Both git channels in `ci.sh`'s `docs_only()` pass `--no-renames`:
rename detection names only the destination, so `git mv src/app.ts notes.md` otherwise read as docs
and skipped the gate over a source file that was gone. `src/ci-gate.test.ts` locks the flags as a
*text* guard — the test image (`node:24.19.0-alpine3.24`) ships neither `git` nor `bash`, so it
cannot exercise the function; behaviour was verified against a scratch repo across ten scenarios.
Revisit if a `.md` ever becomes load-bearing. Decided 2026-08-05.
- **Plainpages is pre-announcement: no tags, no releases.** The repo carried tags up to `v0.2.2` from
the `auto-release` job; all of them — and the semver container tags — were deleted 2026-08-05, and
the job is gated behind the `AUTO_RELEASE` Actions variable (unset ⇒ skipped, the fail-safe
direction on every unknown-`vars` path). A version only communicates to consumers, and there are
none; same reasoning that freezes `HOST_API_VERSION` at 1.0.0. Note the coupling:
`registry-cleanup` keeps a hash image only while its commit is a branch head *or* release-tagged,
so with zero tags only branch heads survive the nightly prune — a hand-cut tag must sit on `main`'s
tip. Valid until the maintainer says Plainpages is ready to show people.
- **A dropdown is a `<button popovertarget>` + `[popover]`, never a `<details>`.** The browser then
owns open/close, which is the only zero-JS way to dismiss a menu by clicking outside it (the whole
point), and the panel sits in the top layer so a row kebab is no longer clipped by `.table-wrap`'s
+7 -4
View File
@@ -1526,7 +1526,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs
| `release.yml` | push of a `vX.Y.Z` tag | re-tag that commit's image as `X.Y.Z`, `X.Y`, `X`, `latest`; sync those tags to Docker Hub |
| `mirror.yml` | push to `main` or any tag, or manual | force-push `main` + tags to the [GitHub mirror](https://github.com/larvit/plainpages) |
| `registry-cleanup.yml` | nightly cron, or manual | delete registry images that are neither release-tagged nor a branch head |
| `renovate.yml` | nightly cron, or manual | open dependency-update PRs, automerge them once the gate is green; the release-tag job only runs when `AUTO_RELEASE` is set |
| `renovate.yml` | nightly cron, or manual | open dependency-update PRs, automerge them once the gate is green; the release-tag job only runs when `AUTO_RELEASE` is `true` |
`main` is not re-tested on push — its commits are meant to arrive already green from a
gated branch, so the status check to gate a merge on is `CI / full-gate (push)`.
@@ -1607,11 +1607,14 @@ instead of tripping the anonymous 60-requests/hour limit.
**Releases are paused.** Plainpages is pre-announcement: the repository carries **no tags**, and
neither `release.yml` nor Docker Hub has a version to promote. Turn releasing back on by setting the
Actions **variable** `AUTO_RELEASE` to `true` (that alone re-enables the job below), or cut a
`vX.Y.Z` tag by hand.
`vX.Y.Z` tag by hand **on `main`'s tip** — with nothing tagged, the nightly registry cleanup keeps
only branch-head images, so an older commit's image is already gone and `release.yml` would fail
loud with nothing to promote.
**Auto-release on dependency updates** — a second job in `renovate.yml` (`auto-release`, `needs:
renovate`, gated on `AUTO_RELEASE` above) cuts **one** `vX.Y.Z` tag per run covering the renovate-bot commits merged to `main`
since the last tag (it targets `origin/main`, and **skips** when the tip isn't a Renovate commit —
renovate`, gated on `AUTO_RELEASE` above) cuts **one** `vX.Y.Z` tag per run covering the
renovate-bot commits merged to `main` since the last tag (it targets `origin/main`, and
**skips** when the tip isn't a Renovate commit —
a human owns that release — or when nothing new merged). Renovate stamps every commit with a
`Release-Bump: <updateType>` trailer (`commitBody` in `renovate.json`), and
[`auto-release/next-version.ts`](auto-release/next-version.ts) (unit-tested) turns the highest
+2 -1
View File
@@ -16,7 +16,8 @@ step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
# The working tree counts too — a dirty tree carrying real code must never skip. Anything
# undeterminable (no git, no reachable main, no merge-base) falls through to the gate, never a skip.
# --no-renames on both channels: rename detection names only the destination, so `git mv src/app.ts
# notes.md` reads as a lone *.md and would skip the gate over a source file that is gone.
# notes.md` reads as a lone *.md — under --porcelain as one `R src/app.ts -> notes.md` line still
# ending in .md after cut -c4- — and the gate would skip over a source file that is gone.
docs_only() {
local base changed
git rev-parse --git-dir >/dev/null 2>&1 || return 1
+2 -4
View File
@@ -8,7 +8,8 @@ import { readFileSync } from "node:fs";
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
const workflow = read(".gitea/workflows/ci.yml");
const gate = read("ci.sh");
// Comments stripped: the flags below must be asserted against the code, not against prose naming them.
const gate = read("ci.sh").split("\n").filter((l) => !l.trimStart().startsWith("#")).join("\n");
const step = (needle: string) => {
const found = workflow.split("\n - ").slice(1).filter((s) => s.includes(needle));
assert.equal(found.length, 1, `exactly one workflow step contains ${needle}`);
@@ -31,8 +32,5 @@ test("the commit-hash image is pushed even when the gate no-ops", () => {
test("only *.md counts as docs; a dirty tree and a rename both count as changed", () => {
assert.ok(gate.includes("\\.md$"), "the non-docs match is a *.md suffix test");
assert.match(gate, /git status --porcelain --no-renames/, "uncommitted code can never be skipped over");
// Rename detection hides the source path: `git mv src/app.ts notes.md` reads as the .md alone
// under --name-only, and as a single `R src/app.ts -> notes.md` line under --porcelain — so
// without --no-renames a deleted source file looks like docs and skips the gate.
assert.match(gate, /git diff --name-only --no-renames/, "a rename must list both of its paths");
});