Skip to content

Commit 699d363

Browse files
committed
fix: align rc release flow with version plans
1 parent d7337f8 commit 699d363

3 files changed

Lines changed: 57 additions & 168 deletions

File tree

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ jobs:
2929
RELEASE_MODE: ${{ github.event.inputs.mode }}
3030
RELEASE_REF: ${{ github.ref_name }}
3131
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3332
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
3433
NPM_CONFIG_PROVENANCE: true
3534
GIT_AUTHOR_NAME: actions-bot

RELEASING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Run the workflow from any branch with `mode=canary`.
2424

2525
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.
2626

27+
Canary releases do not consume or remove version plans.
28+
2729
## Required secrets
2830

2931
The workflow expects `NPM_ACCESS_TOKEN` to be configured in GitHub Actions secrets.

scripts/release/release.mjs

Lines changed: 55 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
#!/usr/bin/env node
22

33
import { execFileSync } from 'node:child_process';
4-
import {
5-
existsSync,
6-
mkdtempSync,
7-
readFileSync,
8-
readdirSync,
9-
rmSync,
10-
writeFileSync,
11-
} from 'node:fs';
12-
import { tmpdir } from 'node:os';
4+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
135
import path from 'node:path';
146
import process from 'node:process';
157
import { ReleaseClient, release } from 'nx/release';
@@ -41,16 +33,6 @@ function isReleaseBranch(ref) {
4133
return /^release\/v\d+\.\d+(?:\.\d+)?$/.test(ref);
4234
}
4335

44-
function parseReleaseBranchVersion(ref) {
45-
const match = ref.match(/^release\/v(\d+)\.(\d+)(?:\.(\d+))?$/);
46-
47-
if (!match) {
48-
return null;
49-
}
50-
51-
return `${match[1]}.${match[2]}.${match[3] ?? '0'}`;
52-
}
53-
5436
function run(command, args, options = {}) {
5537
execFileSync(command, args, {
5638
stdio: 'inherit',
@@ -138,81 +120,25 @@ function getVersionPlans() {
138120
.map((fileName) => path.join(versionPlansDir, fileName));
139121
}
140122

141-
function deleteVersionPlans(versionPlans) {
142-
for (const versionPlan of versionPlans) {
143-
rmSync(versionPlan, { force: true });
144-
}
145-
}
146-
147-
function stageReleaseFiles() {
148-
run('git', [
149-
'add',
150-
'-A',
151-
'.nx/version-plans',
152-
'packages',
153-
'CHANGELOG.md',
154-
'pnpm-lock.yaml',
155-
]);
156-
157-
const staged = runOutput('git', ['diff', '--cached', '--name-only']);
158-
159-
if (staged.length === 0) {
160-
fail('no release files were staged for commit');
161-
}
162-
}
163-
164-
function commitVersionChanges(version) {
165-
stageReleaseFiles();
166-
run('git', ['commit', '-m', `chore: release v${version}`]);
167-
}
168-
169-
function createTag(version) {
170-
const tag = `v${version}`;
171-
172-
if (commandSucceeds('git', ['rev-parse', '--verify', `refs/tags/${tag}`])) {
173-
fail(`tag ${tag} already exists locally`);
174-
}
175-
176-
if (
177-
commandSucceeds('git', [
178-
'ls-remote',
179-
'--exit-code',
180-
'--tags',
181-
remote,
182-
`refs/tags/${tag}`,
183-
])
184-
) {
185-
fail(`tag ${tag} already exists on ${remote}`);
186-
}
187-
188-
run('git', ['tag', tag]);
189-
}
190-
191-
function pushBranchAndTag(version) {
192-
run('git', ['push', remote, `HEAD:${branch}`]);
193-
run('git', ['push', remote, `refs/tags/v${version}`]);
194-
}
195-
196-
function createGitHubRelease(version, notes, prerelease = false) {
197-
ensureGithubToken();
198-
199-
const tempDir = mkdtempSync(path.join(tmpdir(), 'release-notes-'));
200-
const notesPath = path.join(tempDir, 'notes.md');
201-
202-
writeFileSync(notesPath, notes);
203-
204-
const args = ['release', 'create', `v${version}`, '--title', `v${version}`];
205-
206-
if (prerelease) {
207-
args.push('--prerelease');
208-
}
209-
210-
args.push('--notes-file', notesPath, '--target', branch);
211-
212-
try {
213-
run('gh', args);
214-
} finally {
215-
rmSync(tempDir, { recursive: true, force: true });
123+
function convertVersionPlanReleaseType(releaseType) {
124+
switch (releaseType) {
125+
case 'major':
126+
case 'feat!':
127+
case 'fix!':
128+
return 'premajor';
129+
case 'minor':
130+
case 'feat':
131+
return 'preminor';
132+
case 'patch':
133+
case 'fix':
134+
return 'prepatch';
135+
case 'premajor':
136+
case 'preminor':
137+
case 'prepatch':
138+
case 'prerelease':
139+
return releaseType;
140+
default:
141+
return releaseType;
216142
}
217143
}
218144

@@ -226,31 +152,48 @@ function assertPublishSucceeded(results) {
226152
}
227153
}
228154

229-
function getNextRcVersion(targetVersion, currentVersion) {
230-
const parsedCurrent = semver.parse(currentVersion);
155+
function rewriteVersionPlanForRc(content) {
156+
return content.replace(/^---\n([\s\S]*?)\n---/m, (match, frontMatter) => {
157+
const rewrittenFrontMatter = frontMatter.replace(
158+
/^(\s*[^:\n]+:\s*)(\S+)\s*$/gm,
159+
(_, prefix, releaseType) =>
160+
`${prefix}${convertVersionPlanReleaseType(releaseType)}`
161+
);
231162

232-
if (!parsedCurrent) {
233-
fail(`current version ${currentVersion} is not valid semver`);
234-
}
163+
return `---\n${rewrittenFrontMatter}\n---`;
164+
});
165+
}
235166

236-
if (parsedCurrent.version === targetVersion) {
237-
return `${targetVersion}-rc.0`;
238-
}
167+
async function runRcReleaseWithVersionPlans() {
168+
const versionPlans = getVersionPlans();
239169

240-
const prerelease = parsedCurrent.prerelease;
241-
const stableCurrent = `${parsedCurrent.major}.${parsedCurrent.minor}.${parsedCurrent.patch}`;
170+
if (versionPlans.length === 0) {
171+
fail('rc releases require at least one version plan in .nx/version-plans');
172+
}
242173

243-
if (stableCurrent === targetVersion && prerelease[0] === 'rc') {
244-
const nextVersion = semver.inc(currentVersion, 'prerelease', 'rc');
174+
const originalContents = new Map(
175+
versionPlans.map((filePath) => [filePath, readFileSync(filePath, 'utf8')])
176+
);
177+
let releaseSucceeded = false;
245178

246-
if (!nextVersion) {
247-
fail(`could not determine next rc version from ${currentVersion}`);
179+
try {
180+
for (const [filePath, content] of originalContents) {
181+
writeFileSync(filePath, rewriteVersionPlanForRc(content));
248182
}
249183

250-
return nextVersion;
184+
await release({
185+
yes: true,
186+
skipPublish: false,
187+
preid: 'rc',
188+
});
189+
releaseSucceeded = true;
190+
} finally {
191+
if (!releaseSucceeded) {
192+
for (const [filePath, content] of originalContents) {
193+
writeFileSync(filePath, content);
194+
}
195+
}
251196
}
252-
253-
return `${targetVersion}-rc.0`;
254197
}
255198

256199
function getCanaryVersion(currentVersion) {
@@ -300,62 +243,7 @@ async function runRcRelease() {
300243
ensureGithubToken();
301244
ensureNpmToken();
302245

303-
const versionPlans = getVersionPlans();
304-
305-
if (versionPlans.length === 0) {
306-
fail('rc releases require at least one version plan in .nx/version-plans');
307-
}
308-
309-
const targetVersion = parseReleaseBranchVersion(branch);
310-
311-
if (!targetVersion) {
312-
fail(`could not determine target version from ${branch}`);
313-
}
314-
315-
const nextVersion = getNextRcVersion(targetVersion, readVersion());
316-
const releaseClient = new ReleaseClient({});
317-
const { workspaceVersion, projectsVersionData, releaseGraph } =
318-
await releaseClient.releaseVersion({
319-
specifier: nextVersion,
320-
gitCommit: false,
321-
gitTag: false,
322-
stageChanges: true,
323-
deleteVersionPlans: false,
324-
});
325-
const changelogResult = await releaseClient.releaseChangelog({
326-
releaseGraph,
327-
versionData: projectsVersionData,
328-
version: workspaceVersion ?? nextVersion,
329-
gitCommit: false,
330-
gitTag: false,
331-
gitPush: false,
332-
stageChanges: true,
333-
createRelease: false,
334-
deleteVersionPlans: false,
335-
});
336-
337-
deleteVersionPlans(versionPlans);
338-
commitVersionChanges(nextVersion);
339-
createTag(nextVersion);
340-
pushBranchAndTag(nextVersion);
341-
342-
if (changelogResult.workspaceChangelog?.contents) {
343-
createGitHubRelease(
344-
nextVersion,
345-
changelogResult.workspaceChangelog.contents,
346-
true
347-
);
348-
}
349-
350-
const publishResults = await releaseClient.releasePublish({
351-
releaseGraph,
352-
versionData: projectsVersionData,
353-
tag: 'rc',
354-
access: 'public',
355-
outputStyle: 'static',
356-
});
357-
358-
assertPublishSucceeded(publishResults);
246+
await runRcReleaseWithVersionPlans();
359247
}
360248

361249
async function runCanaryRelease() {

0 commit comments

Comments
 (0)