From a865b466bdc0729abcd8d6270d030acda245fdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 11 Sep 2026 21:58:45 +0000 Subject: [PATCH] :construction_worker: Add Gitea release page to CI The publish job now opens a Gitea release for the tag, using the matching Keep-a-Changelog section as the body via scripts/release-notes.sh. It runs before npm publish so a broken page fails CI without burning an npm version; npm publish stays the last step. Uses the first-party gitea-release-action (the older actions/release-action is deprecated and requires asset files) and contents: write for the run's automatic token. --- .gitea/workflows/ci.yml | 17 +++++++++++ CONTRIBUTING.md | 4 +-- backlog.tasks | 3 +- scripts/release-notes.sh | 64 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100755 scripts/release-notes.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b4e9c68..86048d0 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -52,6 +52,10 @@ jobs: if: startsWith(gitea.ref, 'refs/tags/') needs: build runs-on: ubuntu-latest + # The release page is created with the run's automatic Gitea token + # (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`. + permissions: + contents: write steps: # Double-gate: publish only runs on a tag *and* aborts here if NPM_TOKEN # is unset, so a tag push never silently no-ops (or half-publishes). Set @@ -76,6 +80,19 @@ jobs: path: dist/ - run: npm run publish:publint - run: npm run publish:attw + # The Gitea release page is created *before* `npm publish` on + # purpose: a broken page then fails CI without burning an npm + # version. The page is cheap to retry, a published version is not. + # The body is the matching Keep-a-Changelog section; an unknown tag + # makes the extractor exit non-zero, so the page can never go up + # empty. + - name: Extract release notes from CHANGELOG.md + env: + TAG_REF: ${{ gitea.ref }} + run: ./scripts/release-notes.sh "${TAG_REF#refs/tags/}" > release-notes.md + - uses: https://gitea.com/actions/gitea-release-action@v1 + with: + body_path: release-notes.md - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd34ce7..9d76e9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ The tools are organized into a feedback ladder. Each tier catches different thin | `npm run maintain` | manual / CI (advisory) | `maintain:knip` + `maintain:outdated` (whole-project + network scans) | ~10s | | CI build (auto) | on push to `main` | `build` job (build + correctness + packaging) — see [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) | ~30s+ | | CI maintain (auto, non-blocking) | on push to `main` | `npm run maintain` — reports, never fails the build | ~10s | -| CI publish (auto) | on tag | `publish:publint` + `publish:attw`, then `npm publish` | ~10s | +| CI publish (auto) | on tag | Gitea release page (body from CHANGELOG) + `publish:publint` + `publish:attw`, then `npm publish` | ~15s | ### Why these splits? @@ -96,4 +96,4 @@ Publishing is CI-only by policy. Local `npm publish` is not supported. The maint 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. 3. `scripts/release.sh` creates a single release commit (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. +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. diff --git a/backlog.tasks b/backlog.tasks index 9c77da0..9c1d968 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -5,7 +5,8 @@ Backlog and tracking for tiny-pattern-ts. Managed in vscode-todotasks format. --- Setup: -☐ Add gitea release page in CI @high +✔ Add gitea release page in CI @high @done +☐ Manually verify the Gitea release page on a real tag push (needs main) @high ☐ Split off template into separate package => pi --session 01a07dde-7050-7054-bb36-1606d7eb2bc3 @high v1.0: diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh new file mode 100755 index 0000000..64540fa --- /dev/null +++ b/scripts/release-notes.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +set -eu + +# Print the Keep-a-Changelog section for a release tag, so CI can use it as the +# body of the Gitea release page without re-implementing CHANGELOG parsing. +# +# Run as `scripts/release-notes.sh `. A leading `v` is tolerated so both +# `v1.2.3` and `1.2.3` match the `## [1.2.3]` heading. Prints to stdout and +# exits non-zero when the tag has no section, so a release can never publish +# with an empty body. + +TAG="${1:-}" +CHANGELOG="${CHANGELOG:-CHANGELOG.md}" + +if [ -z "${TAG}" ]; then + echo "Error: usage: $0 " >&2 + exit 1 +fi + +if [ ! -f "${CHANGELOG}" ]; then + echo "Error: ${CHANGELOG} not found." >&2 + exit 1 +fi + +# `v1.2.3` and `1.2.3` are the same release; the heading only ever uses the +# bare version. +VERSION="${TAG#v}" + +awk -v version="${VERSION}" ' + # A new section ends the one we are printing (or is the one we want). + /^## \[/ { + if (found) exit + heading = $0 + sub(/^## \[/, "", heading) + sub(/\].*$/, "", heading) + if (heading == version) found = 1 + next + } + + # Link-reference definitions live at the bottom of the file and are never + # part of the section body. Stopping here keeps the last release notes + # from picking them up (there is no following heading to stop at). + /^\[[^]]+\]:/ { exit } + + found { + if ($0 ~ /^[[:space:]]*$/) { + # Buffer blank lines so trailing ones at the end of the section + # are dropped instead of leaking into the release body. + if (started) pending = pending $0 "\n" + } else { + printf "%s%s\n", pending, $0 + pending = "" + started = 1 + } + } + + END { + if (!found) { + printf "Error: no CHANGELOG section for [%s].\n", version > "/dev/stderr" + exit 1 + } + } +' "${CHANGELOG}"