Skip to content

split release plan #1070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/bin/release-plan/commit.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from 'child_process';

export async function commitAfterPackageRelease(newVersion, packageDirectory, packageName) {
export async function commitSingleDirectory(commitMessage, dir) {
await new Promise((resolve, reject) => {
if (process.env.DEBUG) {
resolve('not a real commit');
Expand All @@ -13,10 +13,10 @@ export async function commitAfterPackageRelease(newVersion, packageDirectory, pa
[
'commit',
'-am',
`${packageName} v${newVersion}` // "@csstools/css-tokenizer v1.0.0"
commitMessage
],
{
cwd: packageDirectory
cwd: dir
}
);

Expand Down
34 changes: 1 addition & 33 deletions .github/bin/release-plan/npm-can-publish.mjs
Original file line number Diff line number Diff line change
@@ -1,39 +1,7 @@
import { spawn } from 'child_process';
import { platform } from 'process';

export async function canPublish(packageName) {
const myName = await new Promise((resolve, reject) => {
const whoamiCmd = spawn(
'npm',
[
'whoami'
],
{
shell: platform === 'win32',
stdio: 'pipe'
}
);

let result = '';

whoamiCmd.stdout.on('data', (data) => {
result += data;
});

whoamiCmd.on('close', (code) => {
if (0 !== code) {
reject(new Error(`'npm whoami' exited with code ${code}`));
return;
}

resolve(result.trim());
});
});

if (!myName) {
return false;
}

export async function canPublish(packageName, myName) {
return new Promise((resolve, reject) => {
const accessListCmd = spawn(
'npm',
Expand Down
32 changes: 32 additions & 0 deletions .github/bin/release-plan/npm-whoami.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { spawn } from 'child_process';
import { platform } from 'process';

export async function whoami(packageName) {
return await new Promise((resolve, reject) => {
const whoamiCmd = spawn(
'npm',
[
'whoami'
],
{
shell: platform === 'win32',
stdio: 'pipe'
}
);

let result = '';

whoamiCmd.stdout.on('data', (data) => {
result += data;
});

whoamiCmd.on('close', (code) => {
if (0 !== code) {
reject(new Error(`'npm whoami' exited with code ${code}`));
return;
}

resolve(result.trim());
});
});
}
115 changes: 115 additions & 0 deletions .github/bin/release-plan/prepare-current-release-plan.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { listWorkspaces } from '../list-workspaces/list-workspaces.mjs';
import fs from 'fs/promises'
import path from 'path'
import { currentVersion } from './current-version.mjs';
import { canPublish } from './npm-can-publish.mjs';
import { whoami } from './npm-whoami.mjs';

export async function prepareCurrentReleasePlan() {
const iam = await whoami();
if (!iam) {
throw new Error("Could not determine current npm user");
}

const workspaces = await listWorkspaces();
// Things to release
const needsRelease = new Map();
// Things that should be released after this plan
const maybeNextPlan = new Map();
// Things not to release
const notReleasableNow = new Map();

WORKSPACES_LOOP:
for (const workspace of workspaces) {
if (workspace.private) {
continue;
}

for (const dependency of workspace.dependencies) {
if (needsRelease.has(dependency) || notReleasableNow.has(dependency)) {
notReleasableNow.set(workspace.name, workspace);

let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
if (changelog.includes('Unreleased')) {
maybeNextPlan.set(workspace.name, workspace);
}
// Can not be released before all modified dependencies have been released.
continue WORKSPACES_LOOP;
}
}

let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
if (changelog.includes('Unreleased')) {
const canPublishPackage = await canPublish(workspace.name, iam);
if (!canPublishPackage) {
console.warn("Current npm user does not have write access for", workspace.name);
notReleasableNow.set(workspace.name, workspace);
continue WORKSPACES_LOOP;
}

let increment = '';
if (changelog.includes('Unreleased (patch)')) {
increment = 'patch';
} else if (changelog.includes('Unreleased (minor)')) {
increment = 'minor';
} else if (changelog.includes('Unreleased (major)')) {
increment = 'major';
} else {
console.warn("Invalid CHANGELOG.md in", workspace.name);
notReleasableNow.set(workspace.name, workspace);
continue WORKSPACES_LOOP;
}

workspace.increment = increment;
workspace.changelog = changelog;
needsRelease.set(workspace.name, workspace);
}
}

// Only do a single initial publish at a time
for (const [workspaceName, workspace] of needsRelease) {
const version = await currentVersion(workspace.path);

if (version === '0.0.0') {
const allWorkspaces = new Map(needsRelease);
allWorkspaces.delete(workspaceName);

needsRelease.clear();
needsRelease.set(workspaceName, workspace);

for (const [workspaceName, workspace] of allWorkspaces) {
maybeNextPlan.set(workspaceName, workspace);
notReleasableNow.set(workspaceName, workspace);
}

break;
}
}

if (needsRelease.size === 0) {
console.log('Nothing to release');
process.exit(0);
}

if (maybeNextPlan.size) {
console.log('Excluded:');
for (const workspace of maybeNextPlan.values()) {
console.log(` - ${workspace.name}`);
}
console.log(''); // empty line
}

if (needsRelease.size) {
console.log('Release plan:');
for (const workspace of needsRelease.values()) {
console.log(` - ${workspace.name} (${workspace.increment})`);
}
console.log(''); // empty line
}

return {
needsRelease,
maybeNextPlan,
notReleasableNow,
};
}
77 changes: 77 additions & 0 deletions .github/bin/release-plan/prepare-next-release-plan.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from 'fs/promises'
import path from 'path'
import { addUpdatedPackagesToChangelog } from './add-to-changelog.mjs';

export async function prepareNextReleasePlan(needsRelease, notReleasableNow, maybeNextPlan) {
// Downstream dependents
let didChangeDownstreamPackages = false;

console.log('\nPreparing next plan');

for (const workspace of notReleasableNow.values()) {
const packageInfo = JSON.parse(await fs.readFile(path.join(workspace.path, 'package.json')));
let didChange = false;

let changeLogAdditions = '';

for (const dependency of workspace.dependencies) {
if (needsRelease.has(dependency)) {
const updated = needsRelease.get(dependency);

const dependencyLink = `https://github.com/csstools/postcss-plugins/tree/main/${updated.path.replaceAll('\\', '/')}`;
const nameAsLink = `[\`${updated.name}\`](${dependencyLink})`;
const versionAsLink = `[\`${updated.newVersion}\`](${dependencyLink}/CHANGELOG.md#${updated.newVersionChangeLogHeadingID})`;

if (
packageInfo.dependencies &&
packageInfo.dependencies[updated.name] &&
packageInfo.dependencies[updated.name] !== '*' &&
updated.newVersion
) {
packageInfo.dependencies[updated.name] = '^' + updated.newVersion;

if (updated.newVersion !== '1.0.0') {
// initial releases are not mentioned as updates
changeLogAdditions += `- Updated ${nameAsLink} to ${versionAsLink} (${updated.increment})\n`;
}

didChange = true;
}
if (
packageInfo.devDependencies &&
packageInfo.devDependencies[updated.name] &&
packageInfo.devDependencies[updated.name] !== '*' &&
updated.newVersion
) {
packageInfo.devDependencies[updated.name] = '^' + updated.newVersion;
// dev dependencies are not included in the changelog
didChange = true;
}
if (
packageInfo.peerDependencies &&
packageInfo.peerDependencies[updated.name] &&
packageInfo.peerDependencies[updated.name] !== '*' &&
updated.newVersion
) {
packageInfo.peerDependencies[updated.name] = '^' + updated.newVersion;
changeLogAdditions += `- Updated ${nameAsLink} to ${versionAsLink} (${updated.increment})\n`;
didChange = true;
}
}
}

if (didChange) {
didChangeDownstreamPackages = true;
await fs.writeFile(path.join(workspace.path, 'package.json'), JSON.stringify(packageInfo, null, '\t') + '\n');
}

if (didChange && changeLogAdditions) {
let changelog = (await fs.readFile(path.join(workspace.path, 'CHANGELOG.md'))).toString();
changelog = addUpdatedPackagesToChangelog(workspace, changelog, changeLogAdditions);

await fs.writeFile(path.join(workspace.path, 'CHANGELOG.md'), changelog);
}
}

return didChangeDownstreamPackages;
}
Loading