diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9acaf67..db746a2 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,24 @@ 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: 0 + fetch-tags: true - 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 +43,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 +70,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..fd3ab05 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,8 @@ const parserOptions = { noteKeywords: ['BREAKING CHANGE', 'Breaking Change'] }; 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; @@ -151,9 +154,59 @@ const publish = async (cwd: string) => { return; } - log.info(chalk`\n{cyan Publishing to NPM}`); + const registryOverrideRaw = argv.registry; + if (argv.registry && typeof argv.registry !== 'string') { + throw new TypeError( + `--registry must be a string (e.g. "${DEFAULT_NPM_REGISTRY}"), received ${typeof registryOverrideRaw}: ${String( + argv.registry + )}` + ); + } + + const registry = argv.registry || DEFAULT_NPM_REGISTRY; - await execa('pnpm', ['publish', '--no-git-checks'], { cwd, stdio: 'inherit' }); + log.info(chalk`\n{cyan Publishing to NPM}: {grey ${registry}}`); + + 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')) + .sort(); + + if (tarballs.length !== 1) { + throw new RangeError( + `Expected exactly 1 packed tarball in: ${packDir} for cwd=${cwd} (found ${ + tarballs.length + }): ${tarballs.join(', ')}` + ); + } + + 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'] : []; + + log.info(chalk`{grey Using npm CLI:} ${NPM_CLI_SPEC}`); + + await execa( + 'pnpm', + [ + 'dlx', + NPM_CLI_SPEC, + 'publish', + '--no-git-checks', + '--registry', + registry, + ...provenanceArgs, + tarballPath + ], + { cwd, stdio: 'inherit' } + ); + } finally { + rmSync(packDir, { force: true, recursive: true }); + } }; const pull = async () => {