21 Commits
Author SHA1 Message Date
tmu 1dfb979ebb 🚀 Release 0.1.3
CI / release-gate (push) Successful in 2s
CI / build (push) Successful in 43s
CI / maintain (push) Failing after 33s
CI / publish (push) Failing after 3s
2026-09-14 15:45:11 +00:00
tmu f04ad3d5bc 🔀 Merge chore/fix-ci into main 2026-09-14 15:43:57 +00:00
tmu 93cbfcf6b1 🐛 Expand the node base image through a named stage
CI / release-gate (pull_request) Successful in 2s
CI / build (pull_request) Successful in 37s
CI / publish (pull_request) Skipped
CI / maintain (pull_request) Failing after 34s
COPY --from= resolves its value as a stage name at parse time, before build
args exist, so COPY --from=node:${NODE_VERSION} collapsed to the invalid
'node:' and docker failed with 'failed to parse stage name'. ARGs in global
scope are expanded in FROM, so route through a named nodebase stage; the
per-stage ARG redeclaration keeps the tool-cache paths expanding.
2026-09-14 13:28:29 +00:00
tmu 3e33b51d1b 📝 Document CI runner-image bump ritual
build:runner-image broke the script prefix convention: build is a bare
single-tool command, and no tier fits a docker-daemon + registry-cred action,
so the script leaves package.json and is invoked directly. CONTRIBUTING gains
a 'CI runner image' section recording where the image pushes
(gitea.e1nsnull.de/tmu/act-ci:<version>), the .node-version coupling, and the
three-step Node-bump ritual.
2026-09-14 12:51:34 +00:00
tmu abbdf4410e 📝 Track CI Node-baking work in backlog
Record chore/fix-ci's two done steps and the remaining ops step (build/push
the image; first CI run is the acceptance test). Glyph/tag coupling honoured:
every ✔ line carries @done, open parent stays ☐.
2026-09-14 12:40:22 +00:00
tmu 245dfaf198 💚 Bake Node into the CI job image
setup-node never consults `node` on PATH; its only fast path is a probe of
/opt/hostedtoolcache, which the ephemeral act_runner job containers always
miss, so every job paid a ~50 MB Node download. docker/Dockerfile extends the
runner's default catthehacker/act image with the Node distribution overlaid at
the exact tool-cache layout, so setup-node finds 26.8.2 and skips the fetch
while node-version-file, cache: npm and registry-url keep working unchanged.

Image layers dedupe against the base the host already pulled and prune via
normal docker hygiene — the cleanup story a host bind of /opt/hostedtoolcache
lacks. To make the bake deterministic, .node-version is pinned to the exact
26.8.2 the image carries; scripts/runner-image.sh guards that coupling and
builds/pushes the tag the three node jobs now reference via container.image.

Ops follow-up (outside the repo): build once with
`npm run build:runner-image -- --push` on a machine with registry creds. If
the package is private, the runner needs container registry credentials in
its config.
2026-09-14 12:37:58 +00:00
tmu b9fe21175f 💚 Reuse npm cache in publish job
The publish job ran npm ci against a cold cache on every release while
build/maintain already share a warm node-cache key off the same lockfile.
Tag runs can read caches saved on main, so wiring cache: npm here is free.
2026-09-14 12:13:38 +00:00
tmu 5292455dbc 🚀 Release 0.1.2
CI / release-gate (push) Successful in 2s
CI / build (push) Successful in 36s
CI / maintain (push) Successful in 27s
CI / publish (push) Failing after 3s
2026-09-14 12:03:32 +00:00
tmu d1ef039688 🔀 Merge chore/dependency-updates into main 2026-09-14 12:02:51 +00:00
tmu c65f86e5d5 ⬆️ Upgrade dependencies 2026-09-14 12:02:27 +00:00
tmu b5bfe83140 🚀 Release 0.1.1
CI / release-gate (push) Successful in 2s
CI / build (push) Successful in 34s
CI / maintain (push) Failing after 27s
CI / publish (push) Failing after 4s
2026-09-14 11:58:51 +00:00
tmu 60bf1bb3a9 🔀 Merge chore/dependency-updates into main 2026-09-14 11:58:11 +00:00
tmu 24bd0270c0 ⬆️ Upgrade dependencies 2026-09-14 11:57:34 +00:00
tmu 475136c1e5 📝 Check off done tasks 2026-09-14 11:55:28 +00:00
tmu 67e3e13b5e 🚀 Release 0.1.0
CI / release-gate (push) Successful in 2s
CI / build (push) Successful in 37s
CI / maintain (push) Failing after 30s
CI / publish (push) Failing after 4s
2026-09-14 11:48:55 +00:00
tmu 595759ca7a 🔀 Merge feature/setup into main 2026-09-14 11:47:58 +00:00
tmu 8495adc47b 👷 Retitle release commits to 🚀
The release commit message is a machine-read convention (release-gate
in ci.yml skips the redundant main CI run on it), so the emoji carries
weight: change the mint in scripts/release.sh, the recognized pattern,
and both docs in one atomic commit. No tags or release commits exist
yet, so nothing historical parses differently. 🚀 matches the
gitmoji semantic (deploy/publish) better than 🔖 here.
2026-09-14 11:47:00 +00:00
tmu 3df672d306 👷 Skip main CI run for release commits
A release pushes main and then a tag pointing at the same commit, so
the branch run re-verifies the identical SHA the tag run already
verifies (and publishes). Add a cheap release-gate job that recognizes
the '🔖 Release x.y.z' commit message on main and skips the
full build/maintain jobs; tag, PR, and ordinary main pushes are
unaffected, and the gate fails open (runs CI) if it errors.
2026-09-14 11:41:49 +00:00
tmu 42cbe2194e ♻️ Finalize changelog notes before pubv
pubv's bump heuristic reads [Unreleased], so the maintainer must write the
notes before pubv runs, not after. pubv refuses a dirty tree (its prompt
defaults to No), so the edit is committed as a staging commit and folded back
into pubv's single release commit.
2026-09-14 11:16:47 +00:00
tmu 9dd973a950 🐛 Refresh origin/HEAD before pubv preflight
pubv resolves the default branch from the local refs/remotes/origin/HEAD,
which git fetch never updates, so a clone or default-branch change left it
stale and pubv warned that main was not the default. Refresh it from the
remote before pubv runs, and assert releases are cut from main explicitly.
2026-09-14 11:08:59 +00:00
tmu 034296f011 ✨ Add create:finish merge front door
The merge half of the branching model was still prose, so it drifted per
session. create:finish mirrors create:branch: it asserts the merge-side
preconditions, fast-forwards a stale main (divergence is refused), merges
--no-ff, runs npm run verify, and deletes the branch only when green. The
push stays with create:release so the merge is reviewable first.
2026-09-14 11:08:59 +00:00
13 changed files with 638 additions and 428 deletions

No files matched your search

+59 -5
View File
@@ -12,13 +12,53 @@ on:
workflow_dispatch: {} workflow_dispatch: {}
jobs: jobs:
build: # Cheap gate that collapses the release double-run. `scripts/release.sh`
# pushes `main` and the tag seconds apart, and the tag points at exactly
# the HEAD commit that push delivers — so the branch run would verify the
# identical tree the tag run verifies anyway (plus `publish`). When a push
# to `main` is headed by a release commit (`:rocket: Release x.y.z`, the
# single commit release.sh creates), the full CI is skipped here and the
# tag run becomes the authoritative one for that SHA. All other pushes —
# PRs, tags, ordinary `main` merges — see `skip=false` and run as before.
#
# Coupling: the pattern below MUST stay in sync with the release commit
# message in `scripts/release.sh`. Failure mode if the tag push ever fails
# after `main` accepted the release commit: no CI fires; fix by re-running
# `git push --tags`.
release-gate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Bind-mount the shared pages tree so the coverage step below can write outputs:
# into it. The runner whitelists this path via `container.valid_volumes` skip: ${{ steps.decide.outputs.skip }}
# (docker-space `setup/gitea.sh`); `image` is omitted on purpose so the steps:
# runner keeps using its default job image. - uses: actions/checkout@v4
- id: decide
env:
REF: ${{ gitea.ref }}
run: |
# Keyed on the ref, not just the message: a tag run checks out
# the same release commit, and `publish` needs its `build`.
if [ "${REF}" = "refs/heads/main" ] &&
git log -1 --format=%s | grep -qE '^:rocket: Release [0-9]+\.[0-9]+\.[0-9]+$'; then
echo 'Release commit on main — the tag run covers this SHA; skipping full CI.'
echo 'skip=true' >>"${GITHUB_OUTPUT}"
else
echo 'skip=false' >>"${GITHUB_OUTPUT}"
fi
build:
needs: release-gate
if: needs.release-gate.outputs.skip != 'true'
runs-on: ubuntu-latest
# `image` extends the runner's default job image (catthehacker/act)
# with Node 26 pre-planted in the tool cache layout, so setup-node's
# version probe hits and never downloads (see docker/Dockerfile). The
# tag MUST equal the exact version pinned in `.node-version`; the bump
# ritual is documented in CONTRIBUTING.md § CI runner image. The volume
# bind-mounts the shared pages tree so the
# coverage step below can write into it; the runner whitelists this
# path via `container.valid_volumes` (docker-space `setup/gitea.sh`).
container: container:
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
volumes: volumes:
- /data/gitea-pages:/data/gitea-pages - /data/gitea-pages:/data/gitea-pages
steps: steps:
@@ -69,8 +109,14 @@ jobs:
# the Actions tab for visibility, but must never gate a merge — so # the Actions tab for visibility, but must never gate a merge — so
# continue-on-error and intentionally NOT in `publish`'s `needs`. # continue-on-error and intentionally NOT in `publish`'s `needs`.
maintain: maintain:
needs: release-gate
if: needs.release-gate.outputs.skip != 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
# Same baked image as `build` — without it this job re-downloads Node
# per run (see docker/Dockerfile).
container:
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@@ -84,6 +130,10 @@ jobs:
if: startsWith(gitea.ref, 'refs/tags/') if: startsWith(gitea.ref, 'refs/tags/')
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Same baked image as `build` — setup-node still owns the registry-url
# `.npmrc` rewrite here; only the Node download is skipped.
container:
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
# The release page is created with the run's automatic Gitea token # The release page is created with the run's automatic Gitea token
# (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`. # (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`.
permissions: permissions:
@@ -102,6 +152,10 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: .node-version node-version-file: .node-version
# Same lockfile/key as `build`, and tag runs can read caches
# saved on `main` — without this, every release pays a cold
# `npm ci` despite the warm shared npm cache.
cache: "npm"
registry-url: "https://registry.npmjs.org/" registry-url: "https://registry.npmjs.org/"
- run: npm ci - run: npm ci
# Consume the dist/ that `build` produced and gated, instead of # Consume the dist/ that `build` produced and gated, instead of
+1 -1
View File
@@ -1 +1 @@
26 26.8.2
+21 -1
View File
@@ -7,4 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
[Unreleased]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts ## [0.1.3] - 2026-09-14
- change to custom image for CI
## [0.1.2] - 2026-09-14
- upgrade dependencies
## [0.1.1] - 2026-09-14
- upgrade dependencies
## [0.1.0] - 2026-09-14
- basic setup
[Unreleased]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.3...main
[0.1.3]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.2...0.1.3
[0.1.2]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.1...0.1.2
[0.1.1]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/0.1.0...0.1.1
[0.1.0]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/compare/20308c5a6d8cccfb09b02ac2ebebd8055e91cd11...0.1.0
+16 -4
View File
@@ -87,14 +87,26 @@ This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert
- **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>` - **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>`
- **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it. - **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it.
- **Merging:** `npm run create:finish` (on the branch). It asserts the same clean-tree / no-operation / current-`main` preconditions, fast-forwards a stale `main` (a true divergence is refused), merges the branch `--no-ff`, runs `npm run verify`, and deletes the branch only after the merge is green. The push is deliberately left to `create:release`, so the merge stays local and reviewable — read the diff yourself before finishing. - **Merging:** `npm run create:finish` (on the branch). It asserts the same clean-tree / no-operation / current-`main` preconditions, fast-forwards a stale `main` (a true divergence is refused), merges the branch `--no-ff`, runs `npm run verify`, and deletes the branch only after the merge is green. The push is deliberately left to `create:release`, so the merge stays local and reviewable — read the diff yourself before finishing.
- CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is the authoritative gate. - CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is the authoritative gate. The one exception: a push headed by a release commit (`:rocket: Release x.y.z`) skips the full `build`/`maintain` jobs, because `create:release` pushes the tag for that exact commit right after and the tag run is the authoritative one (see `release-gate` in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml)).
- **Releases are NOT triggered by pushes.** Only the maintainer triggers a release (see [Publishing workflow](#publishing-workflow)). - **Releases are NOT triggered by pushes.** Only the maintainer triggers a release (see [Publishing workflow](#publishing-workflow)).
## CI runner image
The `build` / `maintain` / `publish` jobs run in `gitea.e1nsnull.de/tmu/act-ci:<version>` ([docker/Dockerfile](./docker/Dockerfile)) — the runner's default act image with the Node distribution overlaid at the exact `/opt/hostedtoolcache` layout `actions/setup-node` probes before downloading, so no job pays the ~50 MB fetch. The image tag MUST equal the exact version pinned in `.node-version`; `release-gate` uses no Node and stays on the default image. The script is deliberately NOT an `npm run` script: building requires a docker daemon and registry credentials, so it belongs to no feedback tier — per [Script prefix convention](#script-prefix-convention), no existing prefix fits and that is the signal.
Bumping Node is one coordinated change, committed as a unit:
1. Edit `.node-version` to the new exact `x.y.z` — floats like `26` resolve to the latest patch at runtime and silently bust the baked entry; `scripts/runner-image.sh` refuses them.
2. `docker login gitea.e1nsnull.de` (user + package/access token), then `./scripts/runner-image.sh --push` — it reads the version from `.node-version` and builds/pushes `<IMAGE_REPO>:<version>`.
3. Repoint the three `container.image` tags in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) to the same version.
Skipping step 2 fails CI at image pull; skipping step 3 silently reverts to the per-job download.
## Publishing workflow ## Publishing workflow
Publishing is CI-only by policy. Local `npm publish` is not supported. The maintainer triggers releases from `main`: Publishing is CI-only by policy. Local `npm publish` is not supported. The maintainer triggers releases from `main`:
1. All intended changes are merged to `main` and passing CI. 1. All intended changes are merged to `main` and passing CI.
2. The maintainer runs `npm run create:release` — an interactive prompt suggests a version (based on the latest CHANGELOG entry); the maintainer confirms or edits it. 2. The maintainer runs `npm run create:release`. VS Code opens `CHANGELOG.md` to finalize the `[Unreleased]` notes; because pubv refuses a dirty tree, any edit is committed first (then folded into the release commit), and pubv's interactive prompt suggests a version from those notes — the maintainer confirms or edits it.
3. `scripts/release.sh` creates a single release commit (changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea. 3. `scripts/release.sh` creates a single release commit (graduated changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea.
4. CI runs on the push (the `build` and `maintain` jobs); the `publish` job then fires on the tag, consuming the `dist/` artifact the `build` job produced. The job graph lives in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) — keep that file, not this list, as the source of truth. The publish-tier checks must pass before the artifact is published. The `publish` job also creates the Gitea release page from the matching Keep-a-Changelog section (`scripts/release-notes.sh`); it runs _before_ `npm publish` so a broken page fails CI without consuming a version, and `npm publish` stays the last step. 4. CI fires on both pushes: the `publish` job runs on the tag (`build` + publish-tier checks + release page + `npm publish`), while the branch run's `release-gate` job recognizes the release commit and skips `build`/`maintain` — the tag verifies the identical SHA, so no work is duplicated. The job graph lives in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) — keep that file, not this list, as the source of truth. The publish-tier checks must pass before the artifact is published. The `publish` job also creates the Gitea release page from the matching Keep-a-Changelog section (`scripts/release-notes.sh`); it runs _before_ `npm publish` so a broken page fails CI without consuming a version, and `npm publish` stays the last step.
+5 -1
View File
@@ -35,7 +35,7 @@ Maintenance:
✔ Add a minimal dir-listing webserver to the gitea docker setup (e.g. caddy `file_server browse` reusing the existing reverse proxy, or any single-binary static server, lipanski/docker-static-website) @done (9/13/2026, 9:02:37 PM) ✔ Add a minimal dir-listing webserver to the gitea docker setup (e.g. caddy `file_server browse` reusing the existing reverse proxy, or any single-binary static server, lipanski/docker-static-website) @done (9/13/2026, 9:02:37 PM)
✔ drop the `actions/upload-artifact` coverage step in favour of the shared-dir layout @done (9/13/2026, 10:37:22 PM) ✔ drop the `actions/upload-artifact` coverage step in favour of the shared-dir layout @done (9/13/2026, 10:37:22 PM)
☐ Explore serving coverage for non-tag pushes (e.g. `main/coverage`, PR previews) @low ☐ Explore serving coverage for non-tag pushes (e.g. `main/coverage`, PR previews) @low
☐ Manually verify the coverage was created on a real tag push (needs main) @low ✔ Manually verify the coverage was created on a real tag push (needs main) @low @done (9/14/2026, 1:55:03 PM)
→ design: no deploy step in CI; the webserver just exposes the shared directory (decided over Gitea Pages / Codecov — neither confirmed available/ wanted) → design: no deploy step in CI; the webserver just exposes the shared directory (decided over Gitea Pages / Codecov — neither confirmed available/ wanted)
☐ serve docs over self hosted server @low ☐ serve docs over self hosted server @low
☐ Add a minimal dir-listing webserver to the gitea docker setup for serving docs (reuse existing reverse proxy) ☐ Add a minimal dir-listing webserver to the gitea docker setup for serving docs (reuse existing reverse proxy)
@@ -45,3 +45,7 @@ Maintenance:
☐ Add a minimal dir-listing webserver to the gitea docker setup for serving landing page (reuse existing reverse proxy) ☐ Add a minimal dir-listing webserver to the gitea docker setup for serving landing page (reuse existing reverse proxy)
☐ CI writes landing page to a shared volume keyed by project + tag (e.g. `/landing/tiny-pattern-ts/<tag>/`) ☐ CI writes landing page to a shared volume keyed by project + tag (e.g. `/landing/tiny-pattern-ts/<tag>/`)
☐ Browse to `…/tiny-pattern-ts/index.html` in the browser ☐ Browse to `…/tiny-pattern-ts/index.html` in the browser
☐ Stop Gitea CI re-downloading Node on every job (branch chore/fix-ci)
✔ Share the warm npm cache with the publish job @done
✔ Bake Node into the CI job image (docker/Dockerfile, container.image in ci.yml) @done
☐ Build/push gitea.e1nsnull.de/tmu/act-ci:26.8.2 and confirm setup-node skips the download (first run on the branch = acceptance test) @high
+8
View File
@@ -27,6 +27,14 @@
"knope", "knope",
"runwisp", "runwisp",
"glab", "glab",
"hostedtoolcache",
"nodebase",
"frontends",
"catthehacker",
"nsnull",
"dedup",
"dedupe",
"repoint",
"postversion", "postversion",
"prebuild", "prebuild",
"Zilla", "Zilla",
+44
View File
@@ -0,0 +1,44 @@
# CI job image for the Gitea act_runner: the runner's default job image with
# Node pre-planted where actions/setup-node looks first.
#
# Why this layout: setup-node ignores `node` on PATH; its only fast path is a
# probe of /opt/hostedtoolcache/node/<version>/<arch>. Without an entry there
# it downloads the ~50 MB distribution on EVERY job (the runner's job
# containers are ephemeral, so its tool cache never survives a job). The
# official node images keep exactly the layout setup-node expects under
# /usr/local, so this layer is a pure file overlay — no scripts, no env.
#
# Why not a host bind of /opt/hostedtoolcache: binds never self-prune. Docker
# images are content-addressed: the base layers dedupe against the act image
# the host already has, and `docker image prune` / re-pulls are the cleanup
# story.
#
# NODE_VERSION must match `.node-version` exactly. setup-node resolves a float
# like `26` to the latest known patch at runtime, so a bump silently busts the
# baked entry; `.node-version` is pinned to x.y.z and scripts/runner-image.sh
# guards the coupling. Rebuild + repoint `container.image` in
# .gitea/workflows/ci.yml on every bump.
#
# The extra `nodebase` stage is load-bearing: `COPY --from=` resolves its value
# as a *stage name* at parse time, before build args exist, so
# `COPY --from=node:${NODE_VERSION}` collapses to the invalid `node:` on
# frontends that do not expand args there. ARGs declared before the first FROM
# *are* expanded in FROM, so routing through a named stage works everywhere.
# Global scope: only visible to FROM lines, but that is exactly where we need it.
ARG NODE_VERSION=26.8.2
FROM node:${NODE_VERSION} AS nodebase
FROM catthehacker/ubuntu:act-latest
# ARGs do not cross stage boundaries; redeclare (with the same default, so a
# bare `docker build -f docker/Dockerfile .` still works) for the paths below.
# Keep this default in sync with the global one above.
ARG NODE_VERSION=26.8.2
# node image: bin/ + lib/ under /usr/local → tool cache: bin/ + lib/ under <ver>/x64.
COPY --from=nodebase /usr/local /opt/hostedtoolcache/node/${NODE_VERSION}/x64
# Fail the build (not CI) if the overlay or the version arg were wrong.
# Shell form on purpose: exec form (`RUN [...]`) does not expand ARG values.
RUN "/opt/hostedtoolcache/node/${NODE_VERSION}/x64/bin/node" --version
+406 -406
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "tiny-pattern-ts", "name": "tiny-pattern-ts",
"version": "0.0.0", "version": "0.1.3",
"description": "Pattern matching for TypeScript/ESM environments (F#-style, not regex)", "description": "Pattern matching for TypeScript/ESM environments (F#-style, not regex)",
"keywords": [ "keywords": [
"adt", "adt",
@@ -77,7 +77,7 @@
"expect-type": "1.4.0", "expect-type": "1.4.0",
"knip": "^6.34.0", "knip": "^6.34.0",
"lefthook": "^2.1.12", "lefthook": "^2.1.12",
"oxfmt": "^0.66.0", "oxfmt": "^0.67.0",
"oxlint": "^1.81.0", "oxlint": "^1.81.0",
"oxlint-tsgolint": "^7.0.2001", "oxlint-tsgolint": "^7.0.2001",
"publint": "^0.3.24", "publint": "^0.3.24",
@@ -87,6 +87,6 @@
"node": ">=26" "node": ">=26"
}, },
"allowScripts": { "allowScripts": {
"lefthook@2.1.12": true "lefthook@2.1.14": true
} }
} }
+4 -1
View File
@@ -106,7 +106,10 @@ git show-ref --verify --quiet "refs/heads/${BASE}" || {
# Derive the remote rather than hardcoding it: this repo has `origin` (ssh) and # Derive the remote rather than hardcoding it: this repo has `origin` (ssh) and
# `origin_https`, and `main` tracks the latter — `git fetch origin main` would # `origin_https`, and `main` tracks the latter — `git fetch origin main` would
# check currency against a ref that is never updated here. # check currency against a ref that is never updated here.
UPSTREAM=$(git rev-parse --quiet --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null || true) # `--quiet` echoes the unresolved `main@{upstream}` literal to stdout on
# failure, so it cannot be paired with a `$(...) || fallback`; the non-quiet
# form prints nothing on failure and the fallback runs.
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null) || UPSTREAM=""
if [ -n "${UPSTREAM}" ]; then if [ -n "${UPSTREAM}" ]; then
git fetch --quiet "${UPSTREAM%/*}" "${UPSTREAM#*/}" || { git fetch --quiet "${UPSTREAM%/*}" "${UPSTREAM#*/}" || {
echo "error: '${UPSTREAM}' check failed: could not reach '${UPSTREAM%/*}'." >&2 echo "error: '${UPSTREAM}' check failed: could not reach '${UPSTREAM%/*}'." >&2
+5 -2
View File
@@ -85,7 +85,10 @@ git show-ref --verify --quiet "refs/heads/${BASE}" || {
# Derive the remote rather than hardcoding it: this repo has `origin` (ssh) and # Derive the remote rather than hardcoding it: this repo has `origin` (ssh) and
# `origin_https`, and `main` tracks the latter — `git fetch origin main` would # `origin_https`, and `main` tracks the latter — `git fetch origin main` would
# check currency against a ref that is never updated here. # check currency against a ref that is never updated here.
UPSTREAM=$(git rev-parse --quiet --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null || true) # `--quiet` echoes the unresolved `main@{upstream}` literal to stdout on
# failure, so it cannot be paired with a `$(...) || fallback`; the non-quiet
# form prints nothing on failure and the fallback runs.
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null) || UPSTREAM=""
BEHIND=0 BEHIND=0
if [ -n "${UPSTREAM}" ]; then if [ -n "${UPSTREAM}" ]; then
git fetch --quiet "${UPSTREAM%/*}" "${UPSTREAM#*/}" || { git fetch --quiet "${UPSTREAM%/*}" "${UPSTREAM#*/}" || {
@@ -113,7 +116,7 @@ if [ "${BEHIND}" -ne 0 ]; then
git merge --quiet --ff-only "${UPSTREAM}" git merge --quiet --ff-only "${UPSTREAM}"
fi fi
if ! git merge --quiet --no-ff --no-edit "${START_REF}"; then if ! git merge --quiet --no-ff -m ":twisted_rightwards_arrows: Merge ${START_REF} into ${BASE}" "${START_REF}"; then
echo "error: merge of '${START_REF}' failed; aborting and returning to it." >&2 echo "error: merge of '${START_REF}' failed; aborting and returning to it." >&2
git merge --abort 2>/dev/null || true git merge --abort 2>/dev/null || true
git switch --quiet "${START_REF}" git switch --quiet "${START_REF}"
+33 -4
View File
@@ -13,6 +13,12 @@ set -eu
# package.json + the lockfile; `--amend` folds them into pubv's single commit; # package.json + the lockfile; `--amend` folds them into pubv's single commit;
# tag AFTER the amend (so the tag is never orphaned) and push. # tag AFTER the amend (so the tag is never orphaned) and push.
# #
# The notes are finalized in VS Code *before* pubv: the [Unreleased] body is
# what pubv's bump heuristic reads, so editing afterwards would inform the
# changelog only, not the version choice. pubv refuses a dirty tree, so that
# edit is committed as a staging commit and folded back into the single release
# commit below.
#
# Rejected: the conventional-commits family (our history is gitmoji, not # Rejected: the conventional-commits family (our history is gitmoji, not
# Conventional; and we want hand-written notes); changesets/rtk (config + a # Conventional; and we want hand-written notes); changesets/rtk (config + a
# heavier version/publish flow that fights our CI-only publish); knope/kacl/ # heavier version/publish flow that fights our CI-only publish); knope/kacl/
@@ -52,12 +58,23 @@ if ! git remote set-head origin --auto >/dev/null 2>&1; then
exit 1 exit 1
fi fi
# The [Unreleased] body drives pubv's bump heuristic, so finalize it first.
echo "Opening ${CHANGELOG} in VS Code to finalize the release notes..."
code --wait "${CHANGELOG}"
# pubv refuses a dirty tree (its "continue with a dirty tree?" prompt defaults
# to No), so a changed changelog must be committed before it runs. That commit
# is staging only — the fold below rewrites it into the single release commit.
NOTES_MSG=":memo: Finalize release notes"
if [ -n "$(git status --porcelain -- "${CHANGELOG}")" ]; then
echo "Committing finalized release notes..."
git add "${CHANGELOG}"
git commit -m "${NOTES_MSG}"
fi
echo "Running pubv..." echo "Running pubv..."
pubv --no-tag --no-push --tag-prefix=none pubv --no-tag --no-push --tag-prefix=none
echo "Opening ${CHANGELOG} in VS Code..."
code --wait "${CHANGELOG}"
echo "Reading version from ${CHANGELOG}..." echo "Reading version from ${CHANGELOG}..."
VERSION=$( VERSION=$(
@@ -75,9 +92,21 @@ echo "Release version: ${VERSION}"
echo "Updating package.json and package-lock.json..." echo "Updating package.json and package-lock.json..."
npm version "${VERSION}" --no-git-tag-version npm version "${VERSION}" --no-git-tag-version
# If pubv's graduation commit sits on top of our staging notes commit, drop it
# back into the index so the amend below rewrites the notes commit into the one
# release commit. A message check, not a flag, so a re-run after pubv aborted
# still folds a notes commit left behind by the earlier attempt.
if [ "$(git log -1 --format=%s HEAD~1 2>/dev/null || true)" = "${NOTES_MSG}" ]; then
git reset --soft HEAD~1
fi
echo "Amending release commit..." echo "Amending release commit..."
git add package.json package-lock.json "${CHANGELOG}" git add package.json package-lock.json "${CHANGELOG}"
git commit --amend -m ":bookmark: Release ${VERSION}" # The exact message format is load-bearing: the `release-gate` job in
# .gitea/workflows/ci.yml recognizes `:rocket: Release x.y.z` on main and
# skips the full CI run, since the tag push immediately after verifies the
# identical SHA (and publishes). Keep the two in sync.
git commit --amend -m ":rocket: Release ${VERSION}"
echo "Creating tag ${VERSION}..." echo "Creating tag ${VERSION}..."
git tag "${VERSION}" git tag "${VERSION}"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
# Build (and optionally push) the CI job image from docker/Dockerfile.
# Run wherever docker + registry credentials live (the runner host, or any
# machine that can reach the registry). The registry/repo below MUST match
# the `container.image` references in .gitea/workflows/ci.yml — the runner
# pulls the image by name.
#
# Usage: scripts/runner-image.sh [--push]
IMAGE_REPO="gitea.e1nsnull.de/tmu/act-ci"
NODE_VERSION="$(tr -d '[:space:]' < .node-version)"
if [[ ! "${NODE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "error: .node-version must be pinned to an exact x.y.z, got '${NODE_VERSION}'." >&2
echo " setup-node resolves floats like '26' to the latest patch at runtime," >&2
echo " which silently busts the tool-cache entry baked into the image." >&2
exit 1
fi
IMAGE="${IMAGE_REPO}:${NODE_VERSION}"
# --pull: refresh the act base layer so the derivative does not float on an
# aging default image forever (layer dedup keeps this cheap).
docker build --pull --build-arg "NODE_VERSION=${NODE_VERSION}" -t "${IMAGE}" -f docker/Dockerfile .
if [[ "${1:-}" == "--push" ]]; then
docker push "${IMAGE}"
fi
echo "built ${IMAGE}"
echo "reminder: bump container.image in .gitea/workflows/ci.yml to this tag"