From 36978549222cc5b13bc7da399d0880bbd21546c9 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 19:53:16 +0000 Subject: [PATCH 1/8] fix(repo,versioner): publish with npm via OIDC Switch @dot/versioner to pack with pnpm and publish via an OIDC-capable npm CLI. Update the release workflow and .npmrc to drop token-based auth. --- .github/workflows/release.yml | 42 +++++++++++++++++++---------- .npmrc | 4 +-- packages/versioner/src/versioner.ts | 28 +++++++++++++++++-- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9acaf67..5a3c91b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: branches: - master +permissions: + contents: read + jobs: publish: if: | @@ -15,19 +18,25 @@ jobs: name: release + permissions: + contents: write + id-token: write + steps: - - name: Checkout Commit - uses: actions/checkout@v1 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 100 + fetch-tags: true + ref: master - name: Setup Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 - - name: Checkout Master - run: | - git branch -f master origin/master - git checkout master + - name: Install PNPM + uses: pnpm/action-setup@v4 - name: Sanity Check run: | @@ -35,9 +44,6 @@ jobs: echo node `node -v`; echo pnpm `pnpm -v` - - name: Install PNPM - uses: pnpm/action-setup@v4 - - name: Set Git Config run: | git config pull.rebase false @@ -65,8 +71,16 @@ jobs: # Note: this satisfies aws sdk for @dot/config tests AWS_REGION: 'us-east-1' + - name: OIDC Preflight + shell: bash + run: | + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then + echo "Missing GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN)." >&2 + echo "Ensure the job requests permissions: id-token: write." >&2 + exit 1 + fi + + echo "OIDC env vars detected." + - name: Release and Publish Changed Packages run: pnpm --filter [HEAD^] --workspace-concurrency=1 release - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.npmrc b/.npmrc index 27c7b2b..debe458 100644 --- a/.npmrc +++ b/.npmrc @@ -1,8 +1,8 @@ -//registry.npmjs.org/:_authToken=${NPM_TOKEN} - # npm options auth-type=legacy +# Publishing in CI uses GitHub OIDC (npm Trusted Publisher). For local publishing, authenticate via ~/.npmrc. + # pnpm options always-auth = true enable-pre-post-scripts = true diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index 0e39f60..e17a62d 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -1,7 +1,8 @@ import 'source-map-support'; import { dirname, join, resolve } from 'path'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; import { getLog } from '@dot/log'; import parser from 'conventional-commits-parser'; @@ -25,6 +26,7 @@ const parserOptions = { noteKeywords: ['BREAKING CHANGE', 'Breaking Change'] }; const reBreaking = new RegExp(`(${parserOptions.noteKeywords.join(')|(')})`); +const NPM_CLI_SPEC = 'npm@11.5.1'; type Commit = parser.Commit; @@ -153,7 +155,29 @@ const publish = async (cwd: string) => { log.info(chalk`\n{cyan Publishing to NPM}`); - await execa('pnpm', ['publish', '--no-git-checks'], { cwd, stdio: 'inherit' }); + const packDir = mkdtempSync(join(tmpdir(), 'versioner-pack-')); + try { + await execa('pnpm', ['pack', '--pack-destination', packDir], { cwd, stdio: 'inherit' }); + + const tarballs = readdirSync(packDir).filter((file) => file.endsWith('.tgz')); + const [tarball] = tarballs; + if (!tarball) throw new Error(`Could not find packed tarball in: ${packDir}`); + + const tarballPath = join(packDir, tarball); + const hasOidcEnv = + !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && !!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + const provenanceArgs = hasOidcEnv ? ['--provenance'] : []; + + log.info(chalk`{grey Using npm CLI:} ${NPM_CLI_SPEC}`); + + await execa( + 'pnpm', + ['dlx', NPM_CLI_SPEC, 'publish', '--no-git-checks', ...provenanceArgs, tarballPath], + { cwd, stdio: 'inherit' } + ); + } finally { + rmSync(packDir, { force: true, recursive: true }); + } }; const pull = async () => { From e96045bf3bc5d6be0c02d0a265fe60a92ae1b065 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:18:29 +0000 Subject: [PATCH 2/8] fix(versioner): accept --registry for publish --- packages/versioner/src/versioner.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index e17a62d..aa7fe08 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -27,6 +27,7 @@ const parserOptions = { }; const reBreaking = new RegExp(`(${parserOptions.noteKeywords.join(')|(')})`); const NPM_CLI_SPEC = 'npm@11.5.1'; +const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'; type Commit = parser.Commit; @@ -153,7 +154,14 @@ const publish = async (cwd: string) => { return; } + if (typeof argv.registry !== 'undefined' && typeof argv.registry !== 'string') { + throw new TypeError(`--registry must be a string, received: ${typeof argv.registry}`); + } + + const registry = argv.registry || DEFAULT_NPM_REGISTRY; + log.info(chalk`\n{cyan Publishing to NPM}`); + log.info(chalk`{grey Registry:} ${registry}`); const packDir = mkdtempSync(join(tmpdir(), 'versioner-pack-')); try { @@ -172,7 +180,16 @@ const publish = async (cwd: string) => { await execa( 'pnpm', - ['dlx', NPM_CLI_SPEC, 'publish', '--no-git-checks', ...provenanceArgs, tarballPath], + [ + 'dlx', + NPM_CLI_SPEC, + 'publish', + '--no-git-checks', + '--registry', + registry, + ...provenanceArgs, + tarballPath + ], { cwd, stdio: 'inherit' } ); } finally { From 3ce093e8758e7864c3d0dcf5b071d54900c78dae Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:23:46 +0000 Subject: [PATCH 3/8] fix(versioner): log and normalize publish registry --- packages/versioner/src/versioner.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index aa7fe08..ee4b527 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -27,7 +27,7 @@ const parserOptions = { }; const reBreaking = new RegExp(`(${parserOptions.noteKeywords.join(')|(')})`); const NPM_CLI_SPEC = 'npm@11.5.1'; -const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'; +const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'; type Commit = parser.Commit; @@ -154,13 +154,17 @@ const publish = async (cwd: string) => { return; } - if (typeof argv.registry !== 'undefined' && typeof argv.registry !== 'string') { - throw new TypeError(`--registry must be a string, received: ${typeof argv.registry}`); + if (argv.registry != null && typeof argv.registry !== 'string') { + throw new TypeError( + `--registry must be a string (e.g. "${DEFAULT_NPM_REGISTRY}"), received ${typeof argv.registry}: ${String( + argv.registry + )}` + ); } - const registry = argv.registry || DEFAULT_NPM_REGISTRY; + const registry = (argv.registry || DEFAULT_NPM_REGISTRY).replace(/\/+$/, ''); - log.info(chalk`\n{cyan Publishing to NPM}`); + log.info(chalk`\n{cyan Publishing to registry}`); log.info(chalk`{grey Registry:} ${registry}`); const packDir = mkdtempSync(join(tmpdir(), 'versioner-pack-')); From 85a068937e537428fe02200ce91999b37f81a4f0 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:18:07 +0000 Subject: [PATCH 4/8] fix(repo,versioner): harden npm publish - Fail if pnpm pack emits != 1 tarball. - Add --registry option (default: https://registry.npmjs.org/) and log it. - Release workflow: fetch full git history and publish the triggering SHA. --- .github/workflows/release.yml | 3 +-- packages/versioner/src/versioner.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a3c91b..db746a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 100 + fetch-depth: 0 fetch-tags: true - ref: master - name: Setup Node uses: actions/setup-node@v4 diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index ee4b527..5061c54 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -171,11 +171,19 @@ const publish = async (cwd: string) => { try { await execa('pnpm', ['pack', '--pack-destination', packDir], { cwd, stdio: 'inherit' }); - const tarballs = readdirSync(packDir).filter((file) => file.endsWith('.tgz')); - const [tarball] = tarballs; - if (!tarball) throw new Error(`Could not find packed tarball in: ${packDir}`); + const tarballs = readdirSync(packDir) + .filter((file) => file.endsWith('.tgz')) + .sort(); + + if (tarballs.length !== 1) { + throw new Error( + `Expected exactly 1 packed tarball in: ${packDir} for cwd=${cwd} (found ${ + tarballs.length + }): ${tarballs.join(', ')}` + ); + } - const tarballPath = join(packDir, tarball); + const tarballPath = join(packDir, tarballs[0]); const hasOidcEnv = !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && !!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; const provenanceArgs = hasOidcEnv ? ['--provenance'] : []; From 7eb5ac90243a1d44c27482fc5e2dad1f71887292 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:29:19 +0000 Subject: [PATCH 5/8] fix(versioner): validate registry flag --- packages/versioner/src/versioner.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index 5061c54..e6a8422 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -162,7 +162,12 @@ const publish = async (cwd: string) => { ); } - const registry = (argv.registry || DEFAULT_NPM_REGISTRY).replace(/\/+$/, ''); + const registryOverride = typeof argv.registry === 'string' ? argv.registry.trim() : null; + if (registryOverride != null && registryOverride.length === 0) { + throw new TypeError(`--registry must be a non-empty string (e.g. "${DEFAULT_NPM_REGISTRY}")`); + } + + const registry = (registryOverride || DEFAULT_NPM_REGISTRY).replace(/\/+$/, ''); log.info(chalk`\n{cyan Publishing to registry}`); log.info(chalk`{grey Registry:} ${registry}`); From 31e998372a35313a9a2b32a07a5b1b66b8bf8d29 Mon Sep 17 00:00:00 2001 From: Andrew Powell Date: Sun, 11 Jan 2026 15:29:54 -0500 Subject: [PATCH 6/8] Apply suggestion from @shellscape --- packages/versioner/src/versioner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index e6a8422..63396da 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -181,7 +181,7 @@ const publish = async (cwd: string) => { .sort(); if (tarballs.length !== 1) { - throw new Error( + throw new RangeError( `Expected exactly 1 packed tarball in: ${packDir} for cwd=${cwd} (found ${ tarballs.length }): ${tarballs.join(', ')}` From 60b10d0f7d610bdd8accedbef041c1ba8c0a0cd4 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:35:14 +0000 Subject: [PATCH 7/8] fix(versioner): simplify --registry validation --- packages/versioner/src/versioner.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index 63396da..216bb57 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -154,15 +154,16 @@ const publish = async (cwd: string) => { return; } - if (argv.registry != null && typeof argv.registry !== 'string') { + const registryOverrideRaw = argv.registry; + if (registryOverrideRaw != null && typeof registryOverrideRaw !== 'string') { throw new TypeError( - `--registry must be a string (e.g. "${DEFAULT_NPM_REGISTRY}"), received ${typeof argv.registry}: ${String( - argv.registry + `--registry must be a string (e.g. "${DEFAULT_NPM_REGISTRY}"), received ${typeof registryOverrideRaw}: ${String( + registryOverrideRaw )}` ); } - const registryOverride = typeof argv.registry === 'string' ? argv.registry.trim() : null; + const registryOverride = registryOverrideRaw == null ? null : registryOverrideRaw.trim(); if (registryOverride != null && registryOverride.length === 0) { throw new TypeError(`--registry must be a non-empty string (e.g. "${DEFAULT_NPM_REGISTRY}")`); } From 4fb8a3dcce69c8e0dea31b32be46dc119ac76063 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Sun, 11 Jan 2026 20:44:32 +0000 Subject: [PATCH 8/8] fix(versioner): simplify registry logging --- packages/versioner/src/versioner.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/versioner/src/versioner.ts b/packages/versioner/src/versioner.ts index 216bb57..fd3ab05 100755 --- a/packages/versioner/src/versioner.ts +++ b/packages/versioner/src/versioner.ts @@ -155,23 +155,17 @@ const publish = async (cwd: string) => { } const registryOverrideRaw = argv.registry; - if (registryOverrideRaw != null && typeof registryOverrideRaw !== 'string') { + if (argv.registry && typeof argv.registry !== 'string') { throw new TypeError( `--registry must be a string (e.g. "${DEFAULT_NPM_REGISTRY}"), received ${typeof registryOverrideRaw}: ${String( - registryOverrideRaw + argv.registry )}` ); } - const registryOverride = registryOverrideRaw == null ? null : registryOverrideRaw.trim(); - if (registryOverride != null && registryOverride.length === 0) { - throw new TypeError(`--registry must be a non-empty string (e.g. "${DEFAULT_NPM_REGISTRY}")`); - } - - const registry = (registryOverride || DEFAULT_NPM_REGISTRY).replace(/\/+$/, ''); + const registry = argv.registry || DEFAULT_NPM_REGISTRY; - log.info(chalk`\n{cyan Publishing to registry}`); - log.info(chalk`{grey Registry:} ${registry}`); + log.info(chalk`\n{cyan Publishing to NPM}: {grey ${registry}}`); const packDir = mkdtempSync(join(tmpdir(), 'versioner-pack-')); try {