Prune deleted tags on the GitHub mirror so it stops advertising dropped versions
CI / full-gate (push) Successful in 2m40s
Mirror / github-mirror (push) Successful in 7s

This commit was merged in pull request #55.
This commit is contained in:
2026-08-05 10:42:39 +02:00
parent 2fb5e695e1
commit e5bdc15262
4 changed files with 18 additions and 6 deletions
+4 -1
View File
@@ -12,7 +12,10 @@ jobs:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
fetch-tags: true # load-bearing for --prune below: no local tags would delete every remote one
# --prune so a tag deleted here doesn't live on at GitHub forever. It only removes refs a
# refspec DESTINATION matches — so tags; main is a non-glob dst, other branches match nothing.
- run: |
git push --force \
git push --force --prune \
"https://x-access-token:${{ secrets.MIRROR_GITHUB_TOKEN }}@github.com/larvit/plainpages.git" \
refs/remotes/origin/main:refs/heads/main 'refs/tags/*:refs/tags/*'
+4 -1
View File
@@ -184,7 +184,10 @@ them. Revisit only if the stated reason stops holding.
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.
tip. `mirror.yml` pushes tags with `--prune` so the deletions actually reach the public GitHub
mirror; that makes the runner's tag view load-bearing (hence `fetch-tags: true`) and means a tag
or Release created on GitHub is swept away, so releases are cut on Gitea only. 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
+9 -3
View File
@@ -1524,7 +1524,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs
| --- | --- | --- |
| `ci.yml` | push, any branch except `main` | the full gate (`bash ci.sh`, a no-op on a docs-only branch), then build + push the app image |
| `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) |
| `mirror.yml` | push to `main` or any tag, or manual | force-push `main` + tags (pruning deleted ones) 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 `true` |
@@ -1578,9 +1578,15 @@ Docker Hub when it changes.
**GitHub mirror** — [github.com/larvit/plainpages](https://github.com/larvit/plainpages) is a
read-only mirror; after every merge, `mirror.yml` force-pushes `main` and all tags there,
overwriting any drift (refs deleted on Gitea are not pruned). One-time setup: a dedicated
overwriting any drift. Tags are pushed with `--prune`, so deleting one here deletes it there on
the next mirror run — ref deletions don't trigger the workflow themselves — and the mirror can't
go on advertising a version the source dropped; branches other than `main` are matched by no
refspec and are left alone. The same sweep removes a tag *created* on GitHub, so cut releases on
Gitea, never on the mirror — a GitHub Release made there loses its tag and does not come back.
One-time setup: a dedicated
GitHub machine account with write access to the GitHub repo (whose `main` must not block
force-pushes), and a fine-grained PAT scoped to that repo (Contents: read & write), stored
force-pushes, and which must carry no tag protection — that would reject the prune and fail every
run), and a fine-grained PAT scoped to that repo (Contents: read & write), stored
as the Gitea Actions secret `MIRROR_GITHUB_TOKEN` (repo Settings → Actions → Secrets; Gitea
rejects secret names starting with `GITHUB_`/`GITEA_`). Trigger the workflow manually for
the first sync — until the secret exists, the mirror job fails loud on each merge.
+1 -1
View File
@@ -31,6 +31,6 @@ 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");
assert.match(gate, /git status --porcelain --no-renames/, "uncommitted code and a staged rename can never be skipped over");
assert.match(gate, /git diff --name-only --no-renames/, "a rename must list both of its paths");
});