♻️ Use the automatic token and fail closed on a partial release
Drop the GITEA_TOKEN gate and pass no explicit token: gitea-release-action defaults to the run's automatic github.token, so the release page needs only contents: write. Keep the npm publish gated on NPM_TOKEN, but add a final always() step that fails the job unless both the release page and npm publish reported success, so a skipped npm half is an explicit red job instead of a silently green one.
This commit is contained in:
1 parent
60e416b0fe
commit
79b4d8c005
2 files changed
+44
-31
No files matched your search
+31
-18
@@ -141,17 +141,17 @@ jobs:
|
|||||||
# `.npmrc` rewrite here; only the Node download is skipped.
|
# `.npmrc` rewrite here; only the Node download is skipped.
|
||||||
container:
|
container:
|
||||||
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
|
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
|
||||||
# Lift the optional publish secrets into job-level `env` so the steps
|
# The release page is created with the run's automatic Gitea token
|
||||||
# below can gate on them: `secrets` is not an allowed context in a step
|
# (`github.token`), so it needs `contents: write`.
|
||||||
# `if` (see GitHub's context-availability table), `env` is. An unset
|
permissions:
|
||||||
# secret arrives as the empty string, which is exactly the skip signal.
|
contents: write
|
||||||
# A tag pushed without the maintainer's secrets (a fork, a manual
|
# The npm token is optional: `secrets` is not an allowed context in a
|
||||||
# dispatch) now runs the packaging checks and skips only the publish
|
# step `if` (see GitHub's context-availability table), so it is lifted
|
||||||
# steps whose token is missing, instead of failing the job at an assert.
|
# into job-level `env`, where an unset secret arrives as the empty
|
||||||
# Set both in the Gitea repo: Settings → Actions → Secrets.
|
# string and skips the publish rather than attempting an unauthenticated
|
||||||
|
# one. Set NPM_TOKEN in the Gitea repo: Settings → Actions → Secrets.
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -171,25 +171,38 @@ jobs:
|
|||||||
path: dist/
|
path: dist/
|
||||||
- run: npm run publish:publint
|
- run: npm run publish:publint
|
||||||
- run: npm run publish:attw
|
- run: npm run publish:attw
|
||||||
# When both tokens are present the Gitea release page is created
|
# The Gitea release page is created *before* `npm publish` on
|
||||||
# *before* `npm publish` on purpose: a broken page then fails CI
|
# purpose: a broken page then fails CI without burning an npm
|
||||||
# without burning an npm version. The page is cheap to retry, a
|
# version. The page is cheap to retry, a published version is not.
|
||||||
# published version is not. The body is the matching
|
# The body is the matching Keep-a-Changelog section; an unknown tag
|
||||||
# Keep-a-Changelog section; an unknown tag makes the extractor exit
|
# makes the extractor exit non-zero, so the page can never go up
|
||||||
# non-zero, so the page can never go up empty.
|
# empty.
|
||||||
- name: Extract release notes from CHANGELOG.md
|
- name: Extract release notes from CHANGELOG.md
|
||||||
if: env.GITEA_TOKEN != ''
|
|
||||||
env:
|
env:
|
||||||
TAG_REF: ${{ gitea.ref }}
|
TAG_REF: ${{ gitea.ref }}
|
||||||
run: ./scripts/release-notes.sh "${TAG_REF#refs/tags/}" > release-notes.md
|
run: ./scripts/release-notes.sh "${TAG_REF#refs/tags/}" > release-notes.md
|
||||||
- name: Create the Gitea release
|
- name: Create the Gitea release
|
||||||
if: env.GITEA_TOKEN != ''
|
id: gitea_release
|
||||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ env.GITEA_TOKEN }}
|
|
||||||
body_path: release-notes.md
|
body_path: release-notes.md
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
|
id: npm_publish
|
||||||
if: env.NPM_TOKEN != ''
|
if: env.NPM_TOKEN != ''
|
||||||
run: npm publish --access public
|
run: npm publish --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }}
|
||||||
|
# All-or-nothing: the tag is only released once *both* the release
|
||||||
|
# page and the npm package are up. A skipped npm publish (NPM_TOKEN
|
||||||
|
# unset) has no `success` outcome, so `always()` reaches this check
|
||||||
|
# even after a failure and turns the skipped half into an explicit
|
||||||
|
# red job instead of a silently green one.
|
||||||
|
- name: Require both releases
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
GITEA="${{ steps.gitea_release.outcome }}"
|
||||||
|
NPM="${{ steps.npm_publish.outcome }}"
|
||||||
|
if [ "${GITEA}" != success ] || [ "${NPM}" != success ]; then
|
||||||
|
echo "::error::incomplete release — gitea=${GITEA:-skipped} npm=${NPM:-skipped}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
+13
-13
@@ -45,18 +45,18 @@ Separately, some top-level scripts are **bare** (no prefix): the entry points th
|
|||||||
|
|
||||||
The tools are organized into a feedback ladder. Each tier catches different things at different costs; the rule of thumb is "earlier tiers fire more often, faster tiers catch less, slower tiers are more thorough":
|
The tools are organized into a feedback ladder. Each tier catches different things at different costs; the rule of thumb is "earlier tiers fire more often, faster tiers catch less, slower tiers are more thorough":
|
||||||
|
|
||||||
| Tier | When | What it runs | Time |
|
| Tier | When | What it runs | Time |
|
||||||
| -------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
| -------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||||
| `npm run watch` | manual | `watch:test` — re-runs tests on file save | ~0.1s |
|
| `npm run watch` | manual | `watch:test` — re-runs tests on file save | ~0.1s |
|
||||||
| Pre-commit (auto) | on stage | tsc + oxlint + oxfmt + cspell (staged files only) | ~1.3s |
|
| Pre-commit (auto) | on stage | tsc + oxlint + oxfmt + cspell (staged files only) | ~1.3s |
|
||||||
| Pre-push (auto) | on push | `npm test` (full tsc + unit tests) | ~3.5s |
|
| Pre-push (auto) | on push | `npm test` (full tsc + unit tests) | ~3.5s |
|
||||||
| `npm run check` | manual | Correctness gates: tsc + oxlint + oxfmt + cspell (whole project) | ~3s |
|
| `npm run check` | manual | Correctness gates: tsc + oxlint + oxfmt + cspell (whole project) | ~3s |
|
||||||
| `npm run verify` | manual | Definition of done: `npm run check` + unit tests, one shot | ~6s |
|
| `npm run verify` | manual | Definition of done: `npm run check` + unit tests, one shot | ~6s |
|
||||||
| `npm run fix` | manual | Auto-resolve fixable issues (lint, format) | ~3s |
|
| `npm run fix` | manual | Auto-resolve fixable issues (lint, format) | ~3s |
|
||||||
| `npm run maintain` | manual / CI (advisory) | `maintain:knip` + `maintain:outdated` (whole-project + network scans) | ~10s |
|
| `npm run maintain` | manual / CI (advisory) | `maintain:knip` + `maintain:outdated` (whole-project + network scans) | ~10s |
|
||||||
| CI build (auto) | on push to `main` / tag | `build` job (build + correctness + packaging) — see [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) | ~30s+ |
|
| CI build (auto) | on push to `main` / tag | `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 maintain (auto, non-blocking) | on push to `main` | `npm run maintain` — reports, never fails the build | ~10s |
|
||||||
| CI publish (auto) | on tag | packaging checks + `publish:publint` / `publish:attw`, then a release page (if `GITEA_TOKEN` is set) and `npm publish` (if `NPM_TOKEN` is set) | ~15s |
|
| CI publish (auto) | on tag | packaging checks + `publish:publint` / `publish:attw`, then the Gitea release page and `npm publish` (skipped, and the job failed, without `NPM_TOKEN`) | ~15s |
|
||||||
|
|
||||||
### Why these splits?
|
### Why these splits?
|
||||||
|
|
||||||
@@ -116,4 +116,4 @@ Publishing is CI-only by policy. Local `npm publish` is not supported. The maint
|
|||||||
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.
|
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 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.
|
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.
|
||||||
|
|
||||||
Each publish step is gated on its own secret, so a tag run without the maintainer's secrets (a fork, a manual dispatch) still runs the packaging checks and goes green instead of failing an assert: the release page needs `GITEA_TOKEN` and `npm publish` needs `NPM_TOKEN`. `secrets` is not an allowed context in a step `if`, so the job lifts both into job-level `env` and tests `env.<NAME> != ''`. Create both under Settings → Actions → Secrets: `NPM_TOKEN` with npm publish rights, `GITEA_TOKEN` a Gitea token with repository write access. With both set, the combined run behaves as before.
|
The Gitea release page uses the run's automatic token (`github.token`), so it only needs `contents: write`. `npm publish` is gated on `NPM_TOKEN`, lifted into job-level `env` because `secrets` is not an allowed context in a step `if`: an unset secret skips the publish instead of attempting an unauthenticated one. A tag is all-or-nothing, though — a final `always()` step fails the job unless both the release page and `npm publish` reported `success`, so a skipped or failed npm half turns the job red rather than silently green. Set `NPM_TOKEN` (npm publish rights) under Settings → Actions → Secrets.
|
||||||
Reference in new issue
Block a user