Skip to content

Commit abd5daa

Browse files
authored
split release plan (#1070)
1 parent b0dfbd1 commit abd5daa

File tree

7 files changed

+280
-214
lines changed

7 files changed

+280
-214
lines changed

.github/bin/release-plan/commit.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { spawn } from 'child_process';
22

3-
export async function commitAfterPackageRelease(newVersion, packageDirectory, packageName) {
3+
export async function commitSingleDirectory(commitMessage, dir) {
44
await new Promise((resolve, reject) => {
55
if (process.env.DEBUG) {
66
resolve('not a real commit');
@@ -13,10 +13,10 @@ export async function commitAfterPackageRelease(newVersion, packageDirectory, pa
1313
[
1414
'commit',
1515
'-am',
16-
`${packageName} v${newVersion}` // "@csstools/css-tokenizer v1.0.0"
16+
commitMessage
1717
],
1818
{
19-
cwd: packageDirectory
19+
cwd: dir
2020
}
2121
);
2222

.github/bin/release-plan/npm-can-publish.mjs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,7 @@
11
import { spawn } from 'child_process';
22
import { platform } from 'process';
33

4-
export async function canPublish(packageName) {
5-
const myName = await new Promise((resolve, reject) => {
6-
const whoamiCmd = spawn(
7-
'npm',
8-
[
9-
'whoami'
10-
],
11-
{
12-
shell: platform === 'win32',
13-
stdio: 'pipe'
14-
}
15-
);
16-
17-
let result = '';
18-
19-
whoamiCmd.stdout.on('data', (data) => {
20-
result += data;
21-
});
22-
23-
whoamiCmd.on('close', (code) => {
24-
if (0 !== code) {
25-
reject(new Error(`'npm whoami' exited with code ${code}`));
26-
return;
27-
}
28-
29-
resolve(result.trim());
30-
});
31-
});
32-
33-
if (!myName) {
34-
return false;
35-
}
36-
4+
export async function canPublish(packageName, myName) {
375
return new Promise((resolve, reject) => {
386
const accessListCmd = spawn(
397
'npm',
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { spawn } from 'child_process';
2+
import { platform } from 'process';
3+
4+
export async function whoami(packageName) {
5+
return await new Promise((resolve, reject) => {
6+
const whoamiCmd = spawn(
7+
'npm',
8+
[
9+
'whoami'
10+
],
11+
{
12+
shell: platform === 'win32',
13+
stdio: 'pipe'
14+
}
15+
);
16+
17+
let result = '';
18+
19+
whoamiCmd.stdout.on('data', (data) => {
20+
result += data;
21+
});
22+
23+
whoamiCmd.on('close', (code) => {
24+
if (0 !== code) {
25+
reject(new Error(`'npm whoami' exited with code ${code}`));
26+
return;
27+
}
28+
29+
resolve(result.trim());
30+
});
31+
});
32+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { listWorkspaces } from '../list-workspaces/list-workspaces.mjs';
2+
import fs from 'fs/promises'
3+
import path from 'path'
4+
import { currentVersion } from './current-version.mjs';
5+
import { canPublish } from './npm-can-publish.mjs';
6+
import { whoami } from './npm-whoami.mjs';
7+
8+
export async function prepareCurrentReleasePlan() {
9+
const iam = await whoami();
10+
if (!iam) {
11+
throw new Error("Could not determine current npm user");
12+
}
13+
14+
const workspaces = await listWorkspaces();
15+
// Things to release
16+
const needsRelease = new Map();
17+
// Things that should be released after this plan
18+
const maybeNextPlan = new Map();
19+
// Things not to release
20+
const notReleasableNow = new Map();
21+
22+
WORKSPACES_LOOP:
23+
for (const workspace of workspaces) {
24+
if (workspace.private) {
25+
continue;
26+
}
27+
28+
for (const dependency of workspace.dependencies) {
29+
if (needsRelease.has(dependency) || notReleasableNow.has(dependency)) {
30+
notReleasableNow.set(workspace.name, workspace);
31+
32+
let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
33+
if (changelog.includes('Unreleased')) {
34+
maybeNextPlan.set(workspace.name, workspace);
35+
}
36+
// Can not be released before all modified dependencies have been released.
37+
continue WORKSPACES_LOOP;
38+
}
39+
}
40+
41+
let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
42+
if (changelog.includes('Unreleased')) {
43+
const canPublishPackage = await canPublish(workspace.name, iam);
44+
if (!canPublishPackage) {
45+
console.warn("Current npm user does not have write access for", workspace.name);
46+
notReleasableNow.set(workspace.name, workspace);
47+
continue WORKSPACES_LOOP;
48+
}
49+
50+
let increment = '';
51+
if (changelog.includes('Unreleased (patch)')) {
52+
increment = 'patch';
53+
} else if (changelog.includes('Unreleased (minor)')) {
54+
increment = 'minor';
55+
} else if (changelog.includes('Unreleased (major)')) {
56+
increment = 'major';
57+
} else {
58+
console.warn("Invalid CHANGELOG.md in", workspace.name);
59+
notReleasableNow.set(workspace.name, workspace);
60+
continue WORKSPACES_LOOP;
61+
}
62+
63+
workspace.increment = increment;
64+
workspace.changelog = changelog;
65+
needsRelease.set(workspace.name, workspace);
66+
}
67+
}
68+
69+
// Only do a single initial publish at a time
70+
for (const [workspaceName, workspace] of needsRelease) {
71+
const version = await currentVersion(workspace.path);
72+
73+
if (version === '0.0.0') {
74+
const allWorkspaces = new Map(needsRelease);
75+
allWorkspaces.delete(workspaceName);
76+
77+
needsRelease.clear();
78+
needsRelease.set(workspaceName, workspace);
79+
80+
for (const [workspaceName, workspace] of allWorkspaces) {
81+
maybeNextPlan.set(workspaceName, workspace);
82+
notReleasableNow.set(workspaceName, workspace);
83+
}
84+
85+
break;
86+
}
87+
}
88+
89+
if (needsRelease.size === 0) {
90+
console.log('Nothing to release');
91+
process.exit(0);
92+
}
93+
94+
if (maybeNextPlan.size) {
95+
console.log('Excluded:');
96+
for (const workspace of maybeNextPlan.values()) {
97+
console.log(` - ${workspace.name}`);
98+
}
99+
console.log(''); // empty line
100+
}
101+
102+
if (needsRelease.size) {
103+
console.log('Release plan:');
104+
for (const workspace of needsRelease.values()) {
105+
console.log(` - ${workspace.name} (${workspace.increment})`);
106+
}
107+
console.log(''); // empty line
108+
}
109+
110+
return {
111+
needsRelease,
112+
maybeNextPlan,
113+
notReleasableNow,
114+
};
115+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import fs from 'fs/promises'
2+
import path from 'path'
3+
import { addUpdatedPackagesToChangelog } from './add-to-changelog.mjs';
4+
5+
export async function prepareNextReleasePlan(needsRelease, notReleasableNow, maybeNextPlan) {
6+
// Downstream dependents
7+
let didChangeDownstreamPackages = false;
8+
9+
console.log('\nPreparing next plan');
10+
11+
for (const workspace of notReleasableNow.values()) {
12+
const packageInfo = JSON.parse(await fs.readFile(path.join(workspace.path, 'package.json')));
13+
let didChange = false;
14+
15+
let changeLogAdditions = '';
16+
17+
for (const dependency of workspace.dependencies) {
18+
if (needsRelease.has(dependency)) {
19+
const updated = needsRelease.get(dependency);
20+
21+
const dependencyLink = `https://github.com/csstools/postcss-plugins/tree/main/${updated.path.replaceAll('\\', '/')}`;
22+
const nameAsLink = `[\`${updated.name}\`](${dependencyLink})`;
23+
const versionAsLink = `[\`${updated.newVersion}\`](${dependencyLink}/CHANGELOG.md#${updated.newVersionChangeLogHeadingID})`;
24+
25+
if (
26+
packageInfo.dependencies &&
27+
packageInfo.dependencies[updated.name] &&
28+
packageInfo.dependencies[updated.name] !== '*' &&
29+
updated.newVersion
30+
) {
31+
packageInfo.dependencies[updated.name] = '^' + updated.newVersion;
32+
33+
if (updated.newVersion !== '1.0.0') {
34+
// initial releases are not mentioned as updates
35+
changeLogAdditions += `- Updated ${nameAsLink} to ${versionAsLink} (${updated.increment})\n`;
36+
}
37+
38+
didChange = true;
39+
}
40+
if (
41+
packageInfo.devDependencies &&
42+
packageInfo.devDependencies[updated.name] &&
43+
packageInfo.devDependencies[updated.name] !== '*' &&
44+
updated.newVersion
45+
) {
46+
packageInfo.devDependencies[updated.name] = '^' + updated.newVersion;
47+
// dev dependencies are not included in the changelog
48+
didChange = true;
49+
}
50+
if (
51+
packageInfo.peerDependencies &&
52+
packageInfo.peerDependencies[updated.name] &&
53+
packageInfo.peerDependencies[updated.name] !== '*' &&
54+
updated.newVersion
55+
) {
56+
packageInfo.peerDependencies[updated.name] = '^' + updated.newVersion;
57+
changeLogAdditions += `- Updated ${nameAsLink} to ${versionAsLink} (${updated.increment})\n`;
58+
didChange = true;
59+
}
60+
}
61+
}
62+
63+
if (didChange) {
64+
didChangeDownstreamPackages = true;
65+
await fs.writeFile(path.join(workspace.path, 'package.json'), JSON.stringify(packageInfo, null, '\t') + '\n');
66+
}
67+
68+
if (didChange && changeLogAdditions) {
69+
let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
70+
changelog = addUpdatedPackagesToChangelog(workspace, changelog, changeLogAdditions);
71+
72+
await fs.writeFile(path.join(workspace.path, 'CHANGELOG.md'), changelog);
73+
}
74+
}
75+
76+
return didChangeDownstreamPackages;
77+
}

0 commit comments

Comments
 (0)