From d7337f893bf2506ae07884fbf1778e81d610605b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 3 Apr 2026 08:18:33 +0200 Subject: [PATCH 1/3] chore: automate release workflow --- .github/workflows/release.yml | 64 ++++++ CONTRIBUTING.md | 2 + RELEASING.md | 29 +++ package.json | 3 +- scripts/release/release.mjs | 397 ++++++++++++++++++++++++++++++++++ 5 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 RELEASING.md create mode 100644 scripts/release/release.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f1d156fc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +on: + workflow_dispatch: + inputs: + mode: + description: Release mode to run + required: true + type: choice + options: + - stable + - rc + - canary + +permissions: + contents: write + id-token: write + +concurrency: + group: release-${{ github.event.inputs.mode }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + release: + name: Run release + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + RELEASE_MODE: ${{ github.event.inputs.mode }} + RELEASE_REF: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} + NPM_CONFIG_PROVENANCE: true + GIT_AUTHOR_NAME: actions-bot + GIT_AUTHOR_EMAIL: actions-bot@users.noreply.github.com + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "$GIT_AUTHOR_NAME" + git config user.email "$GIT_AUTHOR_EMAIL" + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 10.6.1 + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.10.0 + cache: pnpm + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run release + run: pnpm release:run diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4fd006e..bf11efa7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,8 @@ This project uses GitHub Actions to run validation checks on your pull requests. Currently, releases are published by maintainers when they determine it's time to do so. Usually, there is at least one release per week as long as there are changes waiting to be published. +The release workflow and branch conventions are documented in [RELEASING.md](/RELEASING.md). + ## License By contributing to React Native Harness, you agree that your contributions will be licensed under its MIT license. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..74942abb --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,29 @@ +# Releasing + +Releases are run with `.github/workflows/release.yml`. + +The workflow always uses the branch it is dispatched from. There is no manual ref input. + +## Stable releases + +Run the workflow from `main` with `mode=stable`. + +This applies pending version plans, publishes packages with the default npm dist-tag, pushes the release commit and tag, and creates the GitHub release. + +## RC releases + +Run the workflow from `release/v` branches, for example `release/v1.1` or `release/v1.1.0`, with `mode=rc`. + +`rc` mode is intentionally restricted to `release/v` branches and will fail on `main`. + +RC releases consume version plans from `.nx/version-plans`, publish packages with the `rc` dist-tag, and create a prerelease on GitHub. + +## Canary releases + +Run the workflow from any branch with `mode=canary`. + +Canary releases publish a unique prerelease version for the current commit with the `canary` dist-tag. They do not create a commit, tag, or GitHub release. + +## Required secrets + +The workflow expects `NPM_ACCESS_TOKEN` to be configured in GitHub Actions secrets. diff --git a/package.json b/package.json index a618d460..8d6f2d20 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "build:all": "nx run-many -t build", "lint:all": "nx run-many -t lint", "test:all": "nx run-many -t test", - "typecheck:all": "nx run-many -t typecheck" + "typecheck:all": "nx run-many -t typecheck", + "release:run": "node ./scripts/release/release.mjs" }, "private": true, "devDependencies": { diff --git a/scripts/release/release.mjs b/scripts/release/release.mjs new file mode 100644 index 00000000..01a27a28 --- /dev/null +++ b/scripts/release/release.mjs @@ -0,0 +1,397 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { ReleaseClient, release } from 'nx/release'; +import semver from 'semver'; + +const cwd = process.cwd(); +const mode = process.env.RELEASE_MODE; +const rawRefName = + process.env.RELEASE_REF || + process.env.GITHUB_REF_NAME || + runOutput('git', ['rev-parse', '--abbrev-ref', 'HEAD']); +const branch = normalizeRef(rawRefName); +const remote = process.env.GIT_REMOTE ?? 'origin'; +const stableBranch = process.env.RELEASE_STABLE_BRANCH ?? 'main'; +const representativePackagePath = + process.env.RELEASE_VERSION_PACKAGE ?? + 'packages/react-native-harness/package.json'; +const versionPlansDir = path.join(cwd, '.nx', 'version-plans'); + +if (!['stable', 'rc', 'canary'].includes(mode)) { + fail('RELEASE_MODE must be set to stable, rc, or canary'); +} + +function normalizeRef(ref) { + return ref.replace(/^refs\/heads\//, '').trim(); +} + +function isReleaseBranch(ref) { + return /^release\/v\d+\.\d+(?:\.\d+)?$/.test(ref); +} + +function parseReleaseBranchVersion(ref) { + const match = ref.match(/^release\/v(\d+)\.(\d+)(?:\.(\d+))?$/); + + if (!match) { + return null; + } + + return `${match[1]}.${match[2]}.${match[3] ?? '0'}`; +} + +function run(command, args, options = {}) { + execFileSync(command, args, { + stdio: 'inherit', + ...options, + }); +} + +function runOutput(command, args, options = {}) { + return execFileSync(command, args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + ...options, + }).trim(); +} + +function commandSucceeds(command, args) { + try { + execFileSync(command, args, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +function fail(message) { + console.error(`Error: ${message}`); + process.exit(1); +} + +function ensureCleanWorktree() { + const status = runOutput('git', ['status', '--porcelain']); + + if (status.length > 0) { + fail('working tree must be clean before running release automation'); + } +} + +function ensureRemoteBranch() { + if ( + !commandSucceeds('git', [ + 'ls-remote', + '--exit-code', + '--heads', + remote, + branch, + ]) + ) { + fail(`branch ${branch} must exist on ${remote}`); + } +} + +function ensureGithubToken() { + if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) { + fail('GITHUB_TOKEN or GH_TOKEN must be set'); + } +} + +function ensureNpmToken() { + if (!process.env.NODE_AUTH_TOKEN) { + fail('NODE_AUTH_TOKEN must be set'); + } +} + +function readVersion() { + const filePath = path.join(cwd, representativePackagePath); + const packageJson = JSON.parse(readFileSync(filePath, 'utf8')); + + if ( + typeof packageJson.version !== 'string' || + packageJson.version.length === 0 + ) { + fail(`could not read version from ${representativePackagePath}`); + } + + return packageJson.version; +} + +function getVersionPlans() { + if (!existsSync(versionPlansDir)) { + return []; + } + + return readdirSync(versionPlansDir) + .filter((fileName) => fileName.endsWith('.md')) + .map((fileName) => path.join(versionPlansDir, fileName)); +} + +function deleteVersionPlans(versionPlans) { + for (const versionPlan of versionPlans) { + rmSync(versionPlan, { force: true }); + } +} + +function stageReleaseFiles() { + run('git', [ + 'add', + '-A', + '.nx/version-plans', + 'packages', + 'CHANGELOG.md', + 'pnpm-lock.yaml', + ]); + + const staged = runOutput('git', ['diff', '--cached', '--name-only']); + + if (staged.length === 0) { + fail('no release files were staged for commit'); + } +} + +function commitVersionChanges(version) { + stageReleaseFiles(); + run('git', ['commit', '-m', `chore: release v${version}`]); +} + +function createTag(version) { + const tag = `v${version}`; + + if (commandSucceeds('git', ['rev-parse', '--verify', `refs/tags/${tag}`])) { + fail(`tag ${tag} already exists locally`); + } + + if ( + commandSucceeds('git', [ + 'ls-remote', + '--exit-code', + '--tags', + remote, + `refs/tags/${tag}`, + ]) + ) { + fail(`tag ${tag} already exists on ${remote}`); + } + + run('git', ['tag', tag]); +} + +function pushBranchAndTag(version) { + run('git', ['push', remote, `HEAD:${branch}`]); + run('git', ['push', remote, `refs/tags/v${version}`]); +} + +function createGitHubRelease(version, notes, prerelease = false) { + ensureGithubToken(); + + const tempDir = mkdtempSync(path.join(tmpdir(), 'release-notes-')); + const notesPath = path.join(tempDir, 'notes.md'); + + writeFileSync(notesPath, notes); + + const args = ['release', 'create', `v${version}`, '--title', `v${version}`]; + + if (prerelease) { + args.push('--prerelease'); + } + + args.push('--notes-file', notesPath, '--target', branch); + + try { + run('gh', args); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +function assertPublishSucceeded(results) { + const failedProjects = Object.entries(results) + .filter(([, result]) => result.code !== 0) + .map(([project]) => project); + + if (failedProjects.length > 0) { + fail(`publishing failed for: ${failedProjects.join(', ')}`); + } +} + +function getNextRcVersion(targetVersion, currentVersion) { + const parsedCurrent = semver.parse(currentVersion); + + if (!parsedCurrent) { + fail(`current version ${currentVersion} is not valid semver`); + } + + if (parsedCurrent.version === targetVersion) { + return `${targetVersion}-rc.0`; + } + + const prerelease = parsedCurrent.prerelease; + const stableCurrent = `${parsedCurrent.major}.${parsedCurrent.minor}.${parsedCurrent.patch}`; + + if (stableCurrent === targetVersion && prerelease[0] === 'rc') { + const nextVersion = semver.inc(currentVersion, 'prerelease', 'rc'); + + if (!nextVersion) { + fail(`could not determine next rc version from ${currentVersion}`); + } + + return nextVersion; + } + + return `${targetVersion}-rc.0`; +} + +function getCanaryVersion(currentVersion) { + const parsedCurrent = semver.parse(currentVersion); + + if (!parsedCurrent) { + fail(`current version ${currentVersion} is not valid semver`); + } + + const stableCurrent = `${parsedCurrent.major}.${parsedCurrent.minor}.${parsedCurrent.patch}`; + const timestamp = new Date() + .toISOString() + .replace(/[-:TZ.]/g, '') + .slice(0, 14); + const shortSha = runOutput('git', ['rev-parse', '--short', 'HEAD']); + + return `${stableCurrent}-canary.${timestamp}.${shortSha}`; +} + +async function runStableRelease() { + if (branch !== stableBranch) { + fail(`stable releases must run from ${stableBranch}, received ${branch}`); + } + + ensureRemoteBranch(); + ensureGithubToken(); + ensureNpmToken(); + + await release({ + yes: true, + skipPublish: false, + }); +} + +async function runRcRelease() { + if (branch === stableBranch) { + fail(`rc releases must not run from ${stableBranch}`); + } + + if (!isReleaseBranch(branch)) { + fail( + `rc releases must run from a release/v branch, received ${branch}` + ); + } + + ensureRemoteBranch(); + ensureGithubToken(); + ensureNpmToken(); + + const versionPlans = getVersionPlans(); + + if (versionPlans.length === 0) { + fail('rc releases require at least one version plan in .nx/version-plans'); + } + + const targetVersion = parseReleaseBranchVersion(branch); + + if (!targetVersion) { + fail(`could not determine target version from ${branch}`); + } + + const nextVersion = getNextRcVersion(targetVersion, readVersion()); + const releaseClient = new ReleaseClient({}); + const { workspaceVersion, projectsVersionData, releaseGraph } = + await releaseClient.releaseVersion({ + specifier: nextVersion, + gitCommit: false, + gitTag: false, + stageChanges: true, + deleteVersionPlans: false, + }); + const changelogResult = await releaseClient.releaseChangelog({ + releaseGraph, + versionData: projectsVersionData, + version: workspaceVersion ?? nextVersion, + gitCommit: false, + gitTag: false, + gitPush: false, + stageChanges: true, + createRelease: false, + deleteVersionPlans: false, + }); + + deleteVersionPlans(versionPlans); + commitVersionChanges(nextVersion); + createTag(nextVersion); + pushBranchAndTag(nextVersion); + + if (changelogResult.workspaceChangelog?.contents) { + createGitHubRelease( + nextVersion, + changelogResult.workspaceChangelog.contents, + true + ); + } + + const publishResults = await releaseClient.releasePublish({ + releaseGraph, + versionData: projectsVersionData, + tag: 'rc', + access: 'public', + outputStyle: 'static', + }); + + assertPublishSucceeded(publishResults); +} + +async function runCanaryRelease() { + ensureNpmToken(); + + const currentVersion = readVersion(); + const canaryVersion = getCanaryVersion(currentVersion); + const releaseClient = new ReleaseClient({ versionPlans: false }); + const { projectsVersionData, releaseGraph } = + await releaseClient.releaseVersion({ + specifier: canaryVersion, + gitCommit: false, + gitTag: false, + stageChanges: false, + }); + const publishResults = await releaseClient.releasePublish({ + releaseGraph, + versionData: projectsVersionData, + tag: 'canary', + access: 'public', + outputStyle: 'static', + }); + + assertPublishSucceeded(publishResults); +} + +ensureCleanWorktree(); + +if (mode === 'stable') { + await runStableRelease(); + process.exit(0); +} + +if (mode === 'rc') { + await runRcRelease(); + process.exit(0); +} + +await runCanaryRelease(); From 699d36356c1ecc86d0e50abe2f5739f50d30ad11 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 3 Apr 2026 08:49:57 +0200 Subject: [PATCH 2/3] fix: align rc release flow with version plans --- .github/workflows/release.yml | 1 - RELEASING.md | 2 + scripts/release/release.mjs | 222 +++++++++------------------------- 3 files changed, 57 insertions(+), 168 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1d156fc..bd400d2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,6 @@ jobs: RELEASE_MODE: ${{ github.event.inputs.mode }} RELEASE_REF: ${{ github.ref_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} NPM_CONFIG_PROVENANCE: true GIT_AUTHOR_NAME: actions-bot diff --git a/RELEASING.md b/RELEASING.md index 74942abb..efd279ea 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -24,6 +24,8 @@ Run the workflow from any branch with `mode=canary`. Canary releases publish a unique prerelease version for the current commit with the `canary` dist-tag. They do not create a commit, tag, or GitHub release. +Canary releases do not consume or remove version plans. + ## Required secrets The workflow expects `NPM_ACCESS_TOKEN` to be configured in GitHub Actions secrets. diff --git a/scripts/release/release.mjs b/scripts/release/release.mjs index 01a27a28..cca8a5ff 100644 --- a/scripts/release/release.mjs +++ b/scripts/release/release.mjs @@ -1,15 +1,7 @@ #!/usr/bin/env node import { execFileSync } from 'node:child_process'; -import { - existsSync, - mkdtempSync, - readFileSync, - readdirSync, - rmSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { ReleaseClient, release } from 'nx/release'; @@ -41,16 +33,6 @@ function isReleaseBranch(ref) { return /^release\/v\d+\.\d+(?:\.\d+)?$/.test(ref); } -function parseReleaseBranchVersion(ref) { - const match = ref.match(/^release\/v(\d+)\.(\d+)(?:\.(\d+))?$/); - - if (!match) { - return null; - } - - return `${match[1]}.${match[2]}.${match[3] ?? '0'}`; -} - function run(command, args, options = {}) { execFileSync(command, args, { stdio: 'inherit', @@ -138,81 +120,25 @@ function getVersionPlans() { .map((fileName) => path.join(versionPlansDir, fileName)); } -function deleteVersionPlans(versionPlans) { - for (const versionPlan of versionPlans) { - rmSync(versionPlan, { force: true }); - } -} - -function stageReleaseFiles() { - run('git', [ - 'add', - '-A', - '.nx/version-plans', - 'packages', - 'CHANGELOG.md', - 'pnpm-lock.yaml', - ]); - - const staged = runOutput('git', ['diff', '--cached', '--name-only']); - - if (staged.length === 0) { - fail('no release files were staged for commit'); - } -} - -function commitVersionChanges(version) { - stageReleaseFiles(); - run('git', ['commit', '-m', `chore: release v${version}`]); -} - -function createTag(version) { - const tag = `v${version}`; - - if (commandSucceeds('git', ['rev-parse', '--verify', `refs/tags/${tag}`])) { - fail(`tag ${tag} already exists locally`); - } - - if ( - commandSucceeds('git', [ - 'ls-remote', - '--exit-code', - '--tags', - remote, - `refs/tags/${tag}`, - ]) - ) { - fail(`tag ${tag} already exists on ${remote}`); - } - - run('git', ['tag', tag]); -} - -function pushBranchAndTag(version) { - run('git', ['push', remote, `HEAD:${branch}`]); - run('git', ['push', remote, `refs/tags/v${version}`]); -} - -function createGitHubRelease(version, notes, prerelease = false) { - ensureGithubToken(); - - const tempDir = mkdtempSync(path.join(tmpdir(), 'release-notes-')); - const notesPath = path.join(tempDir, 'notes.md'); - - writeFileSync(notesPath, notes); - - const args = ['release', 'create', `v${version}`, '--title', `v${version}`]; - - if (prerelease) { - args.push('--prerelease'); - } - - args.push('--notes-file', notesPath, '--target', branch); - - try { - run('gh', args); - } finally { - rmSync(tempDir, { recursive: true, force: true }); +function convertVersionPlanReleaseType(releaseType) { + switch (releaseType) { + case 'major': + case 'feat!': + case 'fix!': + return 'premajor'; + case 'minor': + case 'feat': + return 'preminor'; + case 'patch': + case 'fix': + return 'prepatch'; + case 'premajor': + case 'preminor': + case 'prepatch': + case 'prerelease': + return releaseType; + default: + return releaseType; } } @@ -226,31 +152,48 @@ function assertPublishSucceeded(results) { } } -function getNextRcVersion(targetVersion, currentVersion) { - const parsedCurrent = semver.parse(currentVersion); +function rewriteVersionPlanForRc(content) { + return content.replace(/^---\n([\s\S]*?)\n---/m, (match, frontMatter) => { + const rewrittenFrontMatter = frontMatter.replace( + /^(\s*[^:\n]+:\s*)(\S+)\s*$/gm, + (_, prefix, releaseType) => + `${prefix}${convertVersionPlanReleaseType(releaseType)}` + ); - if (!parsedCurrent) { - fail(`current version ${currentVersion} is not valid semver`); - } + return `---\n${rewrittenFrontMatter}\n---`; + }); +} - if (parsedCurrent.version === targetVersion) { - return `${targetVersion}-rc.0`; - } +async function runRcReleaseWithVersionPlans() { + const versionPlans = getVersionPlans(); - const prerelease = parsedCurrent.prerelease; - const stableCurrent = `${parsedCurrent.major}.${parsedCurrent.minor}.${parsedCurrent.patch}`; + if (versionPlans.length === 0) { + fail('rc releases require at least one version plan in .nx/version-plans'); + } - if (stableCurrent === targetVersion && prerelease[0] === 'rc') { - const nextVersion = semver.inc(currentVersion, 'prerelease', 'rc'); + const originalContents = new Map( + versionPlans.map((filePath) => [filePath, readFileSync(filePath, 'utf8')]) + ); + let releaseSucceeded = false; - if (!nextVersion) { - fail(`could not determine next rc version from ${currentVersion}`); + try { + for (const [filePath, content] of originalContents) { + writeFileSync(filePath, rewriteVersionPlanForRc(content)); } - return nextVersion; + await release({ + yes: true, + skipPublish: false, + preid: 'rc', + }); + releaseSucceeded = true; + } finally { + if (!releaseSucceeded) { + for (const [filePath, content] of originalContents) { + writeFileSync(filePath, content); + } + } } - - return `${targetVersion}-rc.0`; } function getCanaryVersion(currentVersion) { @@ -300,62 +243,7 @@ async function runRcRelease() { ensureGithubToken(); ensureNpmToken(); - const versionPlans = getVersionPlans(); - - if (versionPlans.length === 0) { - fail('rc releases require at least one version plan in .nx/version-plans'); - } - - const targetVersion = parseReleaseBranchVersion(branch); - - if (!targetVersion) { - fail(`could not determine target version from ${branch}`); - } - - const nextVersion = getNextRcVersion(targetVersion, readVersion()); - const releaseClient = new ReleaseClient({}); - const { workspaceVersion, projectsVersionData, releaseGraph } = - await releaseClient.releaseVersion({ - specifier: nextVersion, - gitCommit: false, - gitTag: false, - stageChanges: true, - deleteVersionPlans: false, - }); - const changelogResult = await releaseClient.releaseChangelog({ - releaseGraph, - versionData: projectsVersionData, - version: workspaceVersion ?? nextVersion, - gitCommit: false, - gitTag: false, - gitPush: false, - stageChanges: true, - createRelease: false, - deleteVersionPlans: false, - }); - - deleteVersionPlans(versionPlans); - commitVersionChanges(nextVersion); - createTag(nextVersion); - pushBranchAndTag(nextVersion); - - if (changelogResult.workspaceChangelog?.contents) { - createGitHubRelease( - nextVersion, - changelogResult.workspaceChangelog.contents, - true - ); - } - - const publishResults = await releaseClient.releasePublish({ - releaseGraph, - versionData: projectsVersionData, - tag: 'rc', - access: 'public', - outputStyle: 'static', - }); - - assertPublishSucceeded(publishResults); + await runRcReleaseWithVersionPlans(); } async function runCanaryRelease() { From 3302db84e8a789bf570af10a43aebcfc25d9e2d4 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 3 Apr 2026 08:51:07 +0200 Subject: [PATCH 3/3] chore: use trusted publishing for releases --- .github/workflows/release.yml | 1 - RELEASING.md | 6 ++++-- scripts/release/release.mjs | 10 ---------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd400d2d..f61b8d25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,6 @@ jobs: RELEASE_MODE: ${{ github.event.inputs.mode }} RELEASE_REF: ${{ github.ref_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} NPM_CONFIG_PROVENANCE: true GIT_AUTHOR_NAME: actions-bot GIT_AUTHOR_EMAIL: actions-bot@users.noreply.github.com diff --git a/RELEASING.md b/RELEASING.md index efd279ea..c34578e8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -26,6 +26,8 @@ Canary releases publish a unique prerelease version for the current commit with Canary releases do not consume or remove version plans. -## Required secrets +## Publishing auth -The workflow expects `NPM_ACCESS_TOKEN` to be configured in GitHub Actions secrets. +Publishing is expected to use npm trusted publishing via GitHub Actions OIDC. + +No npm access token is required for the release workflow itself. diff --git a/scripts/release/release.mjs b/scripts/release/release.mjs index cca8a5ff..8f2d48cb 100644 --- a/scripts/release/release.mjs +++ b/scripts/release/release.mjs @@ -90,12 +90,6 @@ function ensureGithubToken() { } } -function ensureNpmToken() { - if (!process.env.NODE_AUTH_TOKEN) { - fail('NODE_AUTH_TOKEN must be set'); - } -} - function readVersion() { const filePath = path.join(cwd, representativePackagePath); const packageJson = JSON.parse(readFileSync(filePath, 'utf8')); @@ -220,7 +214,6 @@ async function runStableRelease() { ensureRemoteBranch(); ensureGithubToken(); - ensureNpmToken(); await release({ yes: true, @@ -241,14 +234,11 @@ async function runRcRelease() { ensureRemoteBranch(); ensureGithubToken(); - ensureNpmToken(); await runRcReleaseWithVersionPlans(); } async function runCanaryRelease() { - ensureNpmToken(); - const currentVersion = readVersion(); const canaryVersion = getCanaryVersion(currentVersion); const releaseClient = new ReleaseClient({ versionPlans: false });