From 5ad95cd434ee5179b126e2ca8b2bc9cae2d81e04 Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Mon, 12 Oct 2026 12:11:09 +0000 Subject: [PATCH 1/4] ci: adopt the shared npm release workflow Co-Authored-By: Codex --- .github/workflows/release.yml | 92 ++++++++++++++++++----------------- README.md | 4 ++ docs/releases.md | 41 ++++++++++++++++ 3 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 docs/releases.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ee1c313..44421ade 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,51 +1,55 @@ +name: Release + on: push: branches: [main] paths: [package.json] workflow_dispatch: -concurrency: release + inputs: + action: + description: Operation + type: choice + options: [verify, stage, finalize] + default: verify + required: true + version: + description: "Required for finalize only, e.g. 1.2.3" + type: string + default: "" + required: false + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + jobs: - Release: - runs-on: ubuntu-latest - environment: deployment + Verify: + if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'verify' permissions: - contents: write # create the tag and the GitHub Release - id-token: write # npm Trusted Publishing - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - id: version - run: | - tag="v$(node -p 'require("./package.json").version')" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then - echo "released=true" >> "$GITHUB_OUTPUT" - fi - - if: steps.version.outputs.released != 'true' - uses: actions/setup-node@v7 - with: - node-version: 24.x - package-manager-cache: false - registry-url: https://registry.npmjs.org - - if: steps.version.outputs.released != 'true' - run: npm install --global npm@11.21.0 - - if: steps.version.outputs.released != 'true' - run: npm ci - - if: steps.version.outputs.released != 'true' - run: npm run build - - if: steps.version.outputs.released != 'true' - run: npx @nlib/cleanup-package-json --file package.json - # Trusted Publishing (OIDC); a maintainer approves the staged version on npmjs.com. - - if: steps.version.outputs.released != 'true' - run: | - name=$(node -p 'require("./package.json").name') - version=$(node -p 'require("./package.json").version') - npm view "$name@$version" version >/dev/null 2>&1 || npm stage publish - - if: steps.version.outputs.released != 'true' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.version.outputs.tag }} - run: | - awk -v h="## $TAG " 'index($0,h)==1{f=1;next} /^## v/{if(f)exit} f' CHANGELOG.md > notes.md - gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --notes-file notes.md + contents: read + uses: nlibjs/actions/.github/workflows/npm-release.yml@986b87befafad2f4f990c934165a90ea37ddbf36 + with: + action: verify + + Stage: + if: github.event_name == 'push' || github.event.inputs.action == 'stage' + permissions: + contents: write + id-token: write + uses: nlibjs/actions/.github/workflows/npm-release.yml@986b87befafad2f4f990c934165a90ea37ddbf36 + with: + action: stage + environment: deployment + + Finalize: + if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'finalize' + permissions: + contents: write + uses: nlibjs/actions/.github/workflows/npm-release.yml@986b87befafad2f4f990c934165a90ea37ddbf36 + with: + action: finalize + environment: deployment + version: ${{ github.event.inputs.version }} diff --git a/README.md b/README.md index 0d02785f..3c676e52 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,7 @@ Then, set `@nlib/lint-commit` to `commit-msg` hook. #!/bin/sh npx @nlib/lint-commit --input "$1" ``` + +## Releases + +Maintainers can follow the [release guide](docs/releases.md). diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 00000000..0b957507 --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,41 @@ +# Maintainer release guide + +The Release workflow pins `nlibjs/actions` to `986b87befafad2f4f990c934165a90ea37ddbf36`, with helper `70dd80ae972e812efcc9606d0ec5d514c8be3982`. + +## Configuration + +Configure npm Trusted Publisher for `@nlib/lint-commit` with these values: + +| Setting | Value | +| --- | --- | +| Organization or user | `nlibjs` | +| Repository | `lint-commit` | +| Workflow filename | `release.yml` | +| Environment | `deployment` | + +Stage and Finalize use the repository's `deployment` environment. Each operation requires its environment approval if protection rules are configured. Verify uses read-only permissions and runs without that environment. Release tooling uses Node 26.x, npm 11.21.0 and locked cleanup-package-json 0.5.0; package compatibility remains defined by package.json and the test workflow. + +## Operations + +1. For a nonpublishing check, select **Release → Run workflow**, choose a branch, select `action=verify`, and leave version empty. Verify runs lint, tests, build, cleanup and tarball checks. +2. Prepare a normal version and changelog update, then merge to main. A package.json push starts Stage automatically. Manual Stage on main accepts the current package version; leave version empty. +3. Stage retains `release-source/v` at the fixed source SHA before preparation. It saves the tarball and integrity in a draft GitHub Release, then submits that exact tarball to npm. +4. Approve the exact version on npmjs.com's **Staged Packages** page with maintainer 2FA. +5. Run Release on `main` with `action=finalize` and the approved version without `v`. Finalize compares public npm with the saved tarball, publishes the draft, verifies the version tag and deletes the matching source branch. + +Manual dispatch defaults to Verify. Stage and Finalize require main. The release concurrency group queues operations without canceling an active run. This package publishes to npm and GitHub Releases. + +## Recovery + +- An existing published npm version and version tag make Stage a successful no-op. +- Keep the draft and source branch if npm staging or the receipt update fails. Inspect npm Staged Packages and public metadata before retrying. An unconfirmed draft stops Stage without resending the package. +- If npm is public, retry Finalize for the same version. It verifies the saved package and source instead of rebuilding current main. Cleanup can be retried after publication without publishing again. +- A source branch at another SHA, duplicate assets, or inconsistent version tags requires inspection. The workflow retains conflicting refs. +- Reset a definitively failed attempt only after confirming no pending/public npm version and no version tag exists. Record the source SHA and preserve any draft assets, then delete the failed draft and matching source branch before staging current main. A failed build may leave only the source branch. +- An older draft or already-outdated queued Stage can encounter GitHub workflow permission restrictions before a source ref exists. Verify the recorded SHA and package state, retain that SHA with `release-source/v`, and retry the failed operation. + +See the shared workflow's [recovery procedure](https://github.com/nlibjs/actions/blob/986b87befafad2f4f990c934165a90ea37ddbf36/README.md#existing-versions-and-failures). + +## Updating the workflow + +Update all three full SHA references together. Confirm that the workflow and its helper pin are preserved in shared main history. Review the caller contracts and compatibility CI before merging. From 6e098c9e40cbb99f1853ad860f368fd1b22afe98 Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Mon, 12 Oct 2026 12:13:48 +0000 Subject: [PATCH 2/4] docs: record shared release adoption verification Co-Authored-By: Codex --- docs/release-verification.md | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/release-verification.md diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 00000000..7d9eab7c --- /dev/null +++ b/docs/release-verification.md @@ -0,0 +1,37 @@ +# Shared release adoption verification + +Verified on 2026-10-12 from candidate `5ad95cd434ee5179b126e2ca8b2bc9cae2d81e04`, based on main `b8911187707ec7b7fee83ca1e3fb52002a48ad24`. + +## Caller behavior + +- Verify, Stage and Finalize pin workflow `986b87befafad2f4f990c934165a90ea37ddbf36`, which pins helper `70dd80ae972e812efcc9606d0ec5d514c8be3982`. Both commits are preserved in shared main. +- Preserve automatic Stage on main/package.json pushes and the release concurrency group. Manual dispatch defaults to Verify; Finalize takes the approved stable version. +- Preserve the `deployment` environment for Stage and Finalize. Verify is read-only without an environment. Stage requests contents/id-token write; Finalize requests contents write. +- Package version 2.0.0, package.json, lockfile, source, and the existing platform/Node compatibility workflow are unchanged. The README gains a maintainer guide link. + +## Validation + +Node 26.11.1, npm 11.21.0, the committed lockfile, cleanup-package-json 0.5.0 and actionlint 1.7.12 were used. + +| Check | Result | +| --- | --- | +| Frozen installation, ESLint, build | Passed | +| Existing AVA suite | All 47 tests passed | +| Production shared Verify | Passed lint/test/build/cleanup/pack with all release-state, source-ref and publication methods guarded | +| Caller/shared workflow contracts | Passed inputs, SHA pins, narrow permissions and environment selection | +| actionlint and whitespace | Passed | +| Installed cleaned tarball | Both CLI aliases accept a valid commit message and reject an invalid one, including paths with spaces | + +## Package comparison + +The comparison packed the same candidate before and after mandatory cleanup. Both tarballs contain 23 files, including `lib/cli.mjs` and declarations. Test files and source maps remain excluded. + +Only `package/package.json` changed: cleanup removed `devDependencies`, `eslintConfig`, `ava`, and `renovate`. Every retained manifest field is unchanged, including dependencies, engines, main, bin, scripts, files, type, repository and publishConfig. The other 22 files are identical in content, mode and type. + +Local cleaned candidate integrity: + +```text +sha512-VxWMDzCWxKiffDaFd3yKW2PHyXtpitYe8sRlaknaIfdKvyzOrjM7K40EBgFHx8/Q6n9I8h9S/yUdCbuzae3SFQ== +``` + +This is verification evidence for a local candidate, not an npm publication. npm 2.0.0 is already public and its version tag exists. Adoption does not change package.json and therefore does not trigger Stage on merge. Confirm the Trusted Publisher configuration in the [maintainer guide](releases.md) before the next normal release. From dac93c9e5499a9700fdcd49298a0ff283b8697ed Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Mon, 12 Oct 2026 13:09:17 +0000 Subject: [PATCH 3/4] docs: keep adoption verification in the pull request Co-Authored-By: Codex --- docs/release-verification.md | 37 ------------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 docs/release-verification.md diff --git a/docs/release-verification.md b/docs/release-verification.md deleted file mode 100644 index 7d9eab7c..00000000 --- a/docs/release-verification.md +++ /dev/null @@ -1,37 +0,0 @@ -# Shared release adoption verification - -Verified on 2026-10-12 from candidate `5ad95cd434ee5179b126e2ca8b2bc9cae2d81e04`, based on main `b8911187707ec7b7fee83ca1e3fb52002a48ad24`. - -## Caller behavior - -- Verify, Stage and Finalize pin workflow `986b87befafad2f4f990c934165a90ea37ddbf36`, which pins helper `70dd80ae972e812efcc9606d0ec5d514c8be3982`. Both commits are preserved in shared main. -- Preserve automatic Stage on main/package.json pushes and the release concurrency group. Manual dispatch defaults to Verify; Finalize takes the approved stable version. -- Preserve the `deployment` environment for Stage and Finalize. Verify is read-only without an environment. Stage requests contents/id-token write; Finalize requests contents write. -- Package version 2.0.0, package.json, lockfile, source, and the existing platform/Node compatibility workflow are unchanged. The README gains a maintainer guide link. - -## Validation - -Node 26.11.1, npm 11.21.0, the committed lockfile, cleanup-package-json 0.5.0 and actionlint 1.7.12 were used. - -| Check | Result | -| --- | --- | -| Frozen installation, ESLint, build | Passed | -| Existing AVA suite | All 47 tests passed | -| Production shared Verify | Passed lint/test/build/cleanup/pack with all release-state, source-ref and publication methods guarded | -| Caller/shared workflow contracts | Passed inputs, SHA pins, narrow permissions and environment selection | -| actionlint and whitespace | Passed | -| Installed cleaned tarball | Both CLI aliases accept a valid commit message and reject an invalid one, including paths with spaces | - -## Package comparison - -The comparison packed the same candidate before and after mandatory cleanup. Both tarballs contain 23 files, including `lib/cli.mjs` and declarations. Test files and source maps remain excluded. - -Only `package/package.json` changed: cleanup removed `devDependencies`, `eslintConfig`, `ava`, and `renovate`. Every retained manifest field is unchanged, including dependencies, engines, main, bin, scripts, files, type, repository and publishConfig. The other 22 files are identical in content, mode and type. - -Local cleaned candidate integrity: - -```text -sha512-VxWMDzCWxKiffDaFd3yKW2PHyXtpitYe8sRlaknaIfdKvyzOrjM7K40EBgFHx8/Q6n9I8h9S/yUdCbuzae3SFQ== -``` - -This is verification evidence for a local candidate, not an npm publication. npm 2.0.0 is already public and its version tag exists. Adoption does not change package.json and therefore does not trigger Stage on merge. Confirm the Trusted Publisher configuration in the [maintainer guide](releases.md) before the next normal release. From e1a3a0dc0333894a52fbf6605da39fc67ad59263 Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Mon, 12 Oct 2026 13:12:32 +0000 Subject: [PATCH 4/4] docs: focus the release guide on package operations Co-Authored-By: Codex --- docs/releases.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 0b957507..51b4ba74 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -13,29 +13,22 @@ Configure npm Trusted Publisher for `@nlib/lint-commit` with these values: | Workflow filename | `release.yml` | | Environment | `deployment` | -Stage and Finalize use the repository's `deployment` environment. Each operation requires its environment approval if protection rules are configured. Verify uses read-only permissions and runs without that environment. Release tooling uses Node 26.x, npm 11.21.0 and locked cleanup-package-json 0.5.0; package compatibility remains defined by package.json and the test workflow. +Stage and Finalize use the repository's `deployment` environment. Each operation requires its environment approval if protection rules are configured. Verify runs without that environment. ## Operations 1. For a nonpublishing check, select **Release → Run workflow**, choose a branch, select `action=verify`, and leave version empty. Verify runs lint, tests, build, cleanup and tarball checks. 2. Prepare a normal version and changelog update, then merge to main. A package.json push starts Stage automatically. Manual Stage on main accepts the current package version; leave version empty. -3. Stage retains `release-source/v` at the fixed source SHA before preparation. It saves the tarball and integrity in a draft GitHub Release, then submits that exact tarball to npm. +3. Check the Stage run and its saved draft for the intended package/version. 4. Approve the exact version on npmjs.com's **Staged Packages** page with maintainer 2FA. -5. Run Release on `main` with `action=finalize` and the approved version without `v`. Finalize compares public npm with the saved tarball, publishes the draft, verifies the version tag and deletes the matching source branch. +5. Run Release on `main` with `action=finalize` and the approved version without `v`. -Manual dispatch defaults to Verify. Stage and Finalize require main. The release concurrency group queues operations without canceling an active run. This package publishes to npm and GitHub Releases. +Manual dispatch defaults to Verify. Stage and Finalize require main. This package publishes to npm and GitHub Releases. ## Recovery -- An existing published npm version and version tag make Stage a successful no-op. -- Keep the draft and source branch if npm staging or the receipt update fails. Inspect npm Staged Packages and public metadata before retrying. An unconfirmed draft stops Stage without resending the package. -- If npm is public, retry Finalize for the same version. It verifies the saved package and source instead of rebuilding current main. Cleanup can be retried after publication without publishing again. -- A source branch at another SHA, duplicate assets, or inconsistent version tags requires inspection. The workflow retains conflicting refs. -- Reset a definitively failed attempt only after confirming no pending/public npm version and no version tag exists. Record the source SHA and preserve any draft assets, then delete the failed draft and matching source branch before staging current main. A failed build may leave only the source branch. -- An older draft or already-outdated queued Stage can encounter GitHub workflow permission restrictions before a source ref exists. Verify the recorded SHA and package state, retain that SHA with `release-source/v`, and retry the failed operation. - -See the shared workflow's [recovery procedure](https://github.com/nlibjs/actions/blob/986b87befafad2f4f990c934165a90ea37ddbf36/README.md#existing-versions-and-failures). +Follow the shared workflow's [recovery procedure](https://github.com/nlibjs/actions/blob/986b87befafad2f4f990c934165a90ea37ddbf36/README.md#existing-versions-and-failures) for failed or partial releases. ## Updating the workflow -Update all three full SHA references together. Confirm that the workflow and its helper pin are preserved in shared main history. Review the caller contracts and compatibility CI before merging. +Update all three full SHA references in `.github/workflows/release.yml` together. Confirm that the workflow and its helper pin are preserved in shared main history, Stage and Finalize still use `deployment`, and the existing Lint and Test checks pass before merging.