@smoothbricks/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -83
- package/dist/cli.js +50 -20
- package/dist/lib/devenv.d.ts +6 -0
- package/dist/lib/devenv.d.ts.map +1 -0
- package/dist/lib/devenv.js +82 -0
- package/dist/lib/workspace.d.ts +5 -0
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +62 -5
- package/dist/monorepo/index.d.ts +6 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +38 -5
- package/dist/monorepo/lockfile.d.ts +5 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +45 -5
- package/dist/monorepo/managed-files.d.ts +1 -1
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +27 -10
- package/dist/monorepo/package-hygiene.js +1 -1
- package/dist/monorepo/package-policy.d.ts +3 -0
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +122 -19
- package/dist/monorepo/packed-manifest.d.ts +4 -0
- package/dist/monorepo/packed-manifest.d.ts.map +1 -0
- package/dist/monorepo/packed-manifest.js +51 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +6 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +4 -1
- package/dist/monorepo/publish-workflow.d.ts +86 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -0
- package/dist/monorepo/publish-workflow.js +343 -0
- package/dist/nx-version-actions.cjs +25 -0
- package/dist/nx-version-actions.d.cts +6 -0
- package/dist/nx-version-actions.d.cts.map +1 -0
- package/dist/release/core.d.ts +50 -0
- package/dist/release/core.d.ts.map +1 -0
- package/dist/release/core.js +97 -0
- package/dist/release/github-release.d.ts +46 -0
- package/dist/release/github-release.d.ts.map +1 -0
- package/dist/release/github-release.js +97 -0
- package/dist/release/index.d.ts +14 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +544 -109
- package/dist/release/npm-auth.d.ts +17 -0
- package/dist/release/npm-auth.d.ts.map +1 -0
- package/dist/release/npm-auth.js +51 -0
- package/dist/release/orchestration.d.ts +48 -0
- package/dist/release/orchestration.d.ts.map +1 -0
- package/dist/release/orchestration.js +105 -0
- package/dist/release/publish-plan.d.ts +17 -0
- package/dist/release/publish-plan.d.ts.map +1 -0
- package/dist/release/publish-plan.js +15 -0
- package/dist/release/retag-unpublished.d.ts +34 -0
- package/dist/release/retag-unpublished.d.ts.map +1 -0
- package/dist/release/retag-unpublished.js +77 -0
- package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
- package/managed/templates/github/workflows/ci.yml +2 -0
- package/package.json +24 -3
- package/src/cli.ts +64 -25
- package/src/lib/devenv.test.ts +28 -0
- package/src/lib/devenv.ts +89 -0
- package/src/lib/workspace.ts +76 -5
- package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +315 -0
- package/src/monorepo/index.ts +45 -5
- package/src/monorepo/lockfile.ts +52 -7
- package/src/monorepo/managed-files.ts +42 -12
- package/src/monorepo/package-hygiene.ts +1 -1
- package/src/monorepo/package-policy.ts +134 -18
- package/src/monorepo/packed-manifest.ts +67 -0
- package/src/monorepo/packed-package.ts +7 -0
- package/src/monorepo/packs/index.ts +4 -0
- package/src/monorepo/publish-workflow.ts +430 -0
- package/src/nx-version-actions.cts +36 -0
- package/src/release/__tests__/core-properties.test.ts +149 -0
- package/src/release/__tests__/core-scenarios.test.ts +177 -0
- package/src/release/__tests__/core.test.ts +73 -0
- package/src/release/__tests__/fixture-repo.test.ts +305 -0
- package/src/release/__tests__/github-release.test.ts +144 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +113 -0
- package/src/release/__tests__/npm-auth.test.ts +129 -0
- package/src/release/__tests__/orchestration.test.ts +202 -0
- package/src/release/__tests__/publish-plan.test.ts +61 -0
- package/src/release/__tests__/retag-unpublished.test.ts +160 -0
- package/src/release/core.ts +170 -0
- package/src/release/github-release.ts +134 -0
- package/src/release/index.ts +662 -116
- package/src/release/npm-auth.ts +122 -0
- package/src/release/orchestration.ts +186 -0
- package/src/release/publish-plan.ts +37 -0
- package/src/release/retag-unpublished.ts +122 -0
- package/managed/templates/github/workflows/publish.yml +0 -136
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { ReleasePackageInfo } from '../core.js';
|
|
3
|
+
import {
|
|
4
|
+
type RetagUnpublishedOptions,
|
|
5
|
+
type RetagUnpublishedShell,
|
|
6
|
+
type RetagUnpublishedTagUpdate,
|
|
7
|
+
retagUnpublished,
|
|
8
|
+
} from '../retag-unpublished.js';
|
|
9
|
+
|
|
10
|
+
const pkg: ReleasePackageInfo = { name: '@scope/pkg', path: 'packages/pkg', version: '1.2.3' };
|
|
11
|
+
|
|
12
|
+
describe('retag unpublished releases', () => {
|
|
13
|
+
it('moves unpublished owned tags, pushes with a remote lease, and dispatches publish auto', async () => {
|
|
14
|
+
const shell = new RecordingRetagShell({
|
|
15
|
+
versionAtRef: '1.2.3',
|
|
16
|
+
remoteTagObjects: new Map([['@scope/pkg@1.2.3', 'old-tag-object']]),
|
|
17
|
+
dispatchSha: 'target-sha',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const updates = await retagUnpublished(shell, retagOptions({ push: true, dispatch: true }));
|
|
21
|
+
|
|
22
|
+
expect(updates).toEqual([{ tag: '@scope/pkg@1.2.3', pkg, expectedRemoteObject: 'old-tag-object' }]);
|
|
23
|
+
expect(shell.movedTags).toEqual([{ tag: '@scope/pkg@1.2.3', ref: 'HEAD' }]);
|
|
24
|
+
expect(shell.pushed).toEqual([[{ tag: '@scope/pkg@1.2.3', expectedRemoteObject: 'old-tag-object' }]]);
|
|
25
|
+
expect(shell.dispatched).toEqual([{ workflow: 'publish.yml', branch: 'main' }]);
|
|
26
|
+
expect(shell.dispatchRefLookups).toEqual(['main']);
|
|
27
|
+
expect(shell.remoteTagLookups).toEqual(['@scope/pkg@1.2.3']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects tags whose package version already exists on npm', async () => {
|
|
31
|
+
const shell = new RecordingRetagShell({ versionAtRef: '1.2.3', npmPublished: true });
|
|
32
|
+
|
|
33
|
+
await expect(retagUnpublished(shell, retagOptions())).rejects.toThrow(
|
|
34
|
+
'Cannot retag @scope/pkg@1.2.3: @scope/pkg@1.2.3 already exists on npm.',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(shell.movedTags).toEqual([]);
|
|
38
|
+
expect(shell.pushed).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects tags whose version does not match the target ref package manifest', async () => {
|
|
42
|
+
const shell = new RecordingRetagShell({ versionAtRef: '1.2.4' });
|
|
43
|
+
|
|
44
|
+
await expect(retagUnpublished(shell, retagOptions())).rejects.toThrow(
|
|
45
|
+
'Release tag @scope/pkg@1.2.3 cannot move to HEAD: packages/pkg/package.json has version 1.2.4, expected 1.2.3.',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects workflow dispatch when the target ref is not the remote branch head', async () => {
|
|
50
|
+
const shell = new RecordingRetagShell({ versionAtRef: '1.2.3', dispatchSha: 'other-sha' });
|
|
51
|
+
|
|
52
|
+
await expect(retagUnpublished(shell, retagOptions({ push: true, dispatch: true }))).rejects.toThrow(
|
|
53
|
+
'Cannot dispatch publish.yml: HEAD resolves to target-sha, but main resolves to other-sha.',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(shell.movedTags).toEqual([]);
|
|
57
|
+
expect(shell.pushed).toEqual([]);
|
|
58
|
+
expect(shell.dispatched).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('dry-runs without moving, pushing, or dispatching tags', async () => {
|
|
62
|
+
const shell = new RecordingRetagShell({ versionAtRef: '1.2.3', dispatchSha: 'target-sha' });
|
|
63
|
+
|
|
64
|
+
await retagUnpublished(shell, retagOptions({ push: true, dispatch: true, dryRun: true }));
|
|
65
|
+
|
|
66
|
+
expect(shell.movedTags).toEqual([]);
|
|
67
|
+
expect(shell.pushed).toEqual([]);
|
|
68
|
+
expect(shell.dispatched).toEqual([]);
|
|
69
|
+
expect(shell.logs).toContain('Would move @scope/pkg@1.2.3 to HEAD (target-sha).');
|
|
70
|
+
expect(shell.logs).toContain('Would push 1 retagged release tag.');
|
|
71
|
+
expect(shell.logs).toContain('Would dispatch publish.yml on main with bump=auto.');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function retagOptions(overrides: Partial<RetagUnpublishedOptions> = {}): RetagUnpublishedOptions {
|
|
76
|
+
return {
|
|
77
|
+
tags: ['@scope/pkg@1.2.3'],
|
|
78
|
+
toRef: 'HEAD',
|
|
79
|
+
push: false,
|
|
80
|
+
dispatch: false,
|
|
81
|
+
dryRun: false,
|
|
82
|
+
branch: 'main',
|
|
83
|
+
workflow: 'publish.yml',
|
|
84
|
+
...overrides,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class RecordingRetagShell implements RetagUnpublishedShell<ReleasePackageInfo> {
|
|
89
|
+
readonly movedTags: Array<{ tag: string; ref: string }> = [];
|
|
90
|
+
readonly pushed: Array<Array<{ tag: string; expectedRemoteObject: string | null }>> = [];
|
|
91
|
+
readonly dispatched: Array<{ workflow: string; branch: string }> = [];
|
|
92
|
+
readonly dispatchRefLookups: string[] = [];
|
|
93
|
+
readonly remoteTagLookups: string[] = [];
|
|
94
|
+
readonly logs: string[] = [];
|
|
95
|
+
private readonly versionAtRef: string | null;
|
|
96
|
+
private readonly npmPublished: boolean;
|
|
97
|
+
private readonly githubReleaseCreated: boolean;
|
|
98
|
+
private readonly remoteTagObjects: Map<string, string>;
|
|
99
|
+
private readonly dispatchSha: string | null;
|
|
100
|
+
|
|
101
|
+
constructor(options: {
|
|
102
|
+
versionAtRef: string | null;
|
|
103
|
+
npmPublished?: boolean;
|
|
104
|
+
githubReleaseCreated?: boolean;
|
|
105
|
+
remoteTagObjects?: Map<string, string>;
|
|
106
|
+
dispatchSha?: string | null;
|
|
107
|
+
}) {
|
|
108
|
+
this.versionAtRef = options.versionAtRef;
|
|
109
|
+
this.npmPublished = options.npmPublished === true;
|
|
110
|
+
this.githubReleaseCreated = options.githubReleaseCreated === true;
|
|
111
|
+
this.remoteTagObjects = options.remoteTagObjects ?? new Map();
|
|
112
|
+
this.dispatchSha = options.dispatchSha ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
listReleasePackages(): ReleasePackageInfo[] {
|
|
116
|
+
return [pkg];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async resolveRef(): Promise<string> {
|
|
120
|
+
return 'target-sha';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async resolveDispatchRef(branch: string): Promise<string | null> {
|
|
124
|
+
this.dispatchRefLookups.push(branch);
|
|
125
|
+
return this.dispatchSha;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async packageVersionAtRef(): Promise<string | null> {
|
|
129
|
+
return this.versionAtRef;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async npmVersionExists(): Promise<boolean> {
|
|
133
|
+
return this.npmPublished;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async githubReleaseExists(): Promise<boolean> {
|
|
137
|
+
return this.githubReleaseCreated;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async remoteTagObject(tag: string): Promise<string | null> {
|
|
141
|
+
this.remoteTagLookups.push(tag);
|
|
142
|
+
return this.remoteTagObjects.get(tag) ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async createOrMoveTag(tag: string, ref: string): Promise<void> {
|
|
146
|
+
this.movedTags.push({ tag, ref });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async pushTags(updates: Array<RetagUnpublishedTagUpdate<ReleasePackageInfo>>): Promise<void> {
|
|
150
|
+
this.pushed.push(updates.map((update) => ({ tag: update.tag, expectedRemoteObject: update.expectedRemoteObject })));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async dispatchPublishWorkflow(workflow: string, branch: string): Promise<void> {
|
|
154
|
+
this.dispatched.push({ workflow, branch });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
log(message: string): void {
|
|
158
|
+
this.logs.push(message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export interface ReleasePackageInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
version: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface GitReleaseTagInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
sha: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MatchedReleaseTag<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
14
|
+
tag: string;
|
|
15
|
+
sha: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
pkg: Package;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReleaseTagRecord<Package extends ReleasePackageInfo = ReleasePackageInfo>
|
|
21
|
+
extends MatchedReleaseTag<Package> {
|
|
22
|
+
needsNpmPublish: boolean;
|
|
23
|
+
needsGithubRelease: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReleaseTarget<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
27
|
+
sha: string;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
packages: Package[];
|
|
30
|
+
npmPackages: Package[];
|
|
31
|
+
githubPackages: Package[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DurableTagState {
|
|
35
|
+
npmPublished: boolean;
|
|
36
|
+
githubReleaseExists: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ReleasePlanningShell {
|
|
40
|
+
listReleaseTagsByCreatorDate(): Promise<GitReleaseTagInfo[]>;
|
|
41
|
+
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
42
|
+
packageVersionAtRef(packagePath: string, ref: string): Promise<string | null>;
|
|
43
|
+
durableTagState(pkg: ReleasePackageInfo, tag: string): Promise<DurableTagState>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function releaseTag(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>): string {
|
|
47
|
+
return `${pkg.name}@${pkg.version}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function npmDistTagForVersion(version: string): string {
|
|
51
|
+
return version.includes('-') ? 'next' : 'latest';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function releasePackageForTag<Package extends Omit<ReleasePackageInfo, 'version'>>(
|
|
55
|
+
packages: Package[],
|
|
56
|
+
tag: string,
|
|
57
|
+
): { pkg: Package; version: string } | null {
|
|
58
|
+
for (const pkg of packages) {
|
|
59
|
+
const prefix = `${pkg.name}@`;
|
|
60
|
+
if (tag.startsWith(prefix)) {
|
|
61
|
+
const version = tag.slice(prefix.length);
|
|
62
|
+
if (!version) {
|
|
63
|
+
throw new Error(`Release tag ${tag} does not include a version.`);
|
|
64
|
+
}
|
|
65
|
+
return { pkg, version };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function classifyReleaseTag<Package extends ReleasePackageInfo>(
|
|
72
|
+
tag: MatchedReleaseTag<Package>,
|
|
73
|
+
state: DurableTagState,
|
|
74
|
+
): ReleaseTagRecord<Package> {
|
|
75
|
+
return {
|
|
76
|
+
...tag,
|
|
77
|
+
needsNpmPublish: !state.npmPublished,
|
|
78
|
+
needsGithubRelease: !state.githubReleaseExists,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function collectOwnedReleaseTagRecords<Package extends Omit<ReleasePackageInfo, 'version'>>(
|
|
83
|
+
packages: Package[],
|
|
84
|
+
ref: string,
|
|
85
|
+
shell: ReleasePlanningShell,
|
|
86
|
+
): Promise<Array<ReleaseTagRecord<Package & { version: string }>>> {
|
|
87
|
+
const records: Array<ReleaseTagRecord<Package & { version: string }>> = [];
|
|
88
|
+
const completedPackages = new Set<string>();
|
|
89
|
+
for (const tag of await shell.listReleaseTagsByCreatorDate()) {
|
|
90
|
+
const match = releasePackageForTag(packages, tag.name);
|
|
91
|
+
if (match && completedPackages.has(match.pkg.name)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!match || !(await shell.isAncestor(tag.sha, ref))) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const versionAtTag = await shell.packageVersionAtRef(match.pkg.path, tag.sha);
|
|
98
|
+
if (versionAtTag !== match.version) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Release tag ${tag.name} points at ${tag.sha.slice(0, 12)}, but ${match.pkg.path}/package.json has version ${
|
|
101
|
+
versionAtTag ?? 'missing'
|
|
102
|
+
}.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const pkg = { ...match.pkg, version: match.version };
|
|
106
|
+
const state = await shell.durableTagState(pkg, tag.name);
|
|
107
|
+
records.push(classifyReleaseTag({ tag: tag.name, sha: tag.sha, timestamp: tag.timestamp, pkg }, state));
|
|
108
|
+
if (state.npmPublished && state.githubReleaseExists) {
|
|
109
|
+
completedPackages.add(pkg.name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return records;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function groupReleaseTargets<Package extends ReleasePackageInfo>(
|
|
116
|
+
records: ReleaseTagRecord<Package>[],
|
|
117
|
+
): ReleaseTarget<Package>[] {
|
|
118
|
+
const targets = new Map<string, ReleaseTarget<Package>>();
|
|
119
|
+
const packageNamesByTarget = new Map<string, Set<string>>();
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
let target = targets.get(record.sha);
|
|
122
|
+
let packageNames = packageNamesByTarget.get(record.sha);
|
|
123
|
+
if (!target) {
|
|
124
|
+
target = { sha: record.sha, timestamp: record.timestamp, packages: [], npmPackages: [], githubPackages: [] };
|
|
125
|
+
targets.set(record.sha, target);
|
|
126
|
+
packageNames = new Set<string>();
|
|
127
|
+
packageNamesByTarget.set(record.sha, packageNames);
|
|
128
|
+
} else if (record.timestamp > target.timestamp) {
|
|
129
|
+
target.timestamp = record.timestamp;
|
|
130
|
+
}
|
|
131
|
+
if (!packageNames) {
|
|
132
|
+
throw new Error(`Release target ${record.sha.slice(0, 12)} lost package tracking state.`);
|
|
133
|
+
}
|
|
134
|
+
if (packageNames.has(record.pkg.name)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Release target ${record.sha.slice(0, 12)} has more than one release tag for ${record.pkg.name}.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
packageNames.add(record.pkg.name);
|
|
140
|
+
if (record.needsNpmPublish || record.needsGithubRelease) {
|
|
141
|
+
target.packages.push(record.pkg);
|
|
142
|
+
}
|
|
143
|
+
if (record.needsNpmPublish) {
|
|
144
|
+
target.npmPackages.push(record.pkg);
|
|
145
|
+
}
|
|
146
|
+
if (record.needsGithubRelease) {
|
|
147
|
+
target.githubPackages.push(record.pkg);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...targets.values()].sort(
|
|
151
|
+
(left, right) => right.timestamp - left.timestamp || right.sha.localeCompare(left.sha),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function pendingReleaseTargets<Package extends ReleasePackageInfo>(
|
|
156
|
+
records: ReleaseTagRecord<Package>[],
|
|
157
|
+
headSha: string,
|
|
158
|
+
): ReleaseTarget<Package>[] {
|
|
159
|
+
const pending: ReleaseTarget<Package>[] = [];
|
|
160
|
+
for (const target of groupReleaseTargets(records)) {
|
|
161
|
+
if (target.sha === headSha) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (target.npmPackages.length === 0 && target.githubPackages.length === 0) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
pending.push(target);
|
|
168
|
+
}
|
|
169
|
+
return pending.reverse();
|
|
170
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { ChangelogOptions } from 'nx/src/command-line/release/command-object.js';
|
|
5
|
+
import type { NxReleaseConfiguration } from 'nx/src/config/nx-json.js';
|
|
6
|
+
import { type ReleasePackageInfo, releaseTag } from './core.js';
|
|
7
|
+
|
|
8
|
+
const nxRenderOnlyReleaseConfig = {
|
|
9
|
+
changelog: {
|
|
10
|
+
workspaceChangelog: false,
|
|
11
|
+
projectChangelogs: {
|
|
12
|
+
createRelease: false,
|
|
13
|
+
file: false,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
} satisfies NxReleaseConfiguration;
|
|
17
|
+
|
|
18
|
+
interface ProjectChangelogLookupResult {
|
|
19
|
+
projectChangelogs?: Record<string, { contents: string }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RenderNxProjectChangelogInput<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
23
|
+
root: string;
|
|
24
|
+
pkg: Package;
|
|
25
|
+
previousTag: string | null;
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GithubReleaseWriteShell {
|
|
30
|
+
githubReleaseExists(tag: string): Promise<boolean>;
|
|
31
|
+
runGhRelease(args: string[]): Promise<void>;
|
|
32
|
+
log(message: string): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function renderNxProjectChangelogContents(input: RenderNxProjectChangelogInput): Promise<string> {
|
|
36
|
+
return withNxWorkspaceRoot(input.root, async () => {
|
|
37
|
+
const { createAPI } = await import('nx/src/command-line/release/changelog.js');
|
|
38
|
+
const result = await createAPI(
|
|
39
|
+
nxRenderOnlyReleaseConfig,
|
|
40
|
+
false,
|
|
41
|
+
)(nxProjectChangelogArgs(input.pkg, input.previousTag, input.dryRun));
|
|
42
|
+
return projectChangelogContents(result, input.pkg.name);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createOrUpdateGithubRelease(
|
|
47
|
+
pkg: ReleasePackageInfo,
|
|
48
|
+
contents: string,
|
|
49
|
+
shell: GithubReleaseWriteShell,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const tag = releaseTag(pkg);
|
|
52
|
+
const releaseExists = await shell.githubReleaseExists(tag);
|
|
53
|
+
shell.log(`${pkg.name}@${pkg.version}: ${releaseExists ? 'updating' : 'creating'} GitHub Release for ${tag}.`);
|
|
54
|
+
|
|
55
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-github-release-'));
|
|
56
|
+
const notesFile = join(tempDir, 'notes.md');
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(notesFile, contents);
|
|
59
|
+
await shell.runGhRelease(githubReleaseCommandArgs(tag, notesFile, releaseExists, pkg.version));
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: string | null, dryRun: boolean) {
|
|
66
|
+
const base = {
|
|
67
|
+
version: pkg.version,
|
|
68
|
+
projects: [pkg.name],
|
|
69
|
+
gitCommit: false,
|
|
70
|
+
gitTag: false,
|
|
71
|
+
gitPush: false,
|
|
72
|
+
stageChanges: false,
|
|
73
|
+
createRelease: false,
|
|
74
|
+
deleteVersionPlans: false,
|
|
75
|
+
dryRun,
|
|
76
|
+
} satisfies ChangelogOptions;
|
|
77
|
+
return previousTag ? { ...base, from: previousTag } : { ...base, firstRelease: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function projectChangelogContents(result: ProjectChangelogLookupResult, projectName: string): string {
|
|
81
|
+
const changelog = result.projectChangelogs?.[projectName];
|
|
82
|
+
if (!changelog) {
|
|
83
|
+
throw new Error(`Nx did not generate a project changelog for ${projectName}.`);
|
|
84
|
+
}
|
|
85
|
+
return changelog.contents;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function githubReleaseCommandArgs(
|
|
89
|
+
tag: string,
|
|
90
|
+
notesFile: string,
|
|
91
|
+
releaseExists: boolean,
|
|
92
|
+
version: string,
|
|
93
|
+
): string[] {
|
|
94
|
+
const args = [
|
|
95
|
+
'release',
|
|
96
|
+
releaseExists ? 'edit' : 'create',
|
|
97
|
+
tag,
|
|
98
|
+
'--title',
|
|
99
|
+
tag,
|
|
100
|
+
'--notes-file',
|
|
101
|
+
notesFile,
|
|
102
|
+
'--verify-tag',
|
|
103
|
+
`--latest=${isPrereleaseVersion(version) ? 'false' : 'true'}`,
|
|
104
|
+
];
|
|
105
|
+
if (isPrereleaseVersion(version)) {
|
|
106
|
+
args.push('--prerelease');
|
|
107
|
+
}
|
|
108
|
+
return args;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isPrereleaseVersion(version: string): boolean {
|
|
112
|
+
return version.includes('-');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function withNxWorkspaceRoot<T>(root: string, run: () => Promise<T>): Promise<T> {
|
|
116
|
+
const workspaceRootModule = await import('nx/src/utils/workspace-root.js');
|
|
117
|
+
const previousWorkspaceRoot = workspaceRootModule.workspaceRoot;
|
|
118
|
+
const previousEnvWorkspaceRoot = process.env.NX_WORKSPACE_ROOT_PATH;
|
|
119
|
+
const previousCwd = process.cwd();
|
|
120
|
+
process.env.NX_WORKSPACE_ROOT_PATH = root;
|
|
121
|
+
process.chdir(root);
|
|
122
|
+
workspaceRootModule.setWorkspaceRoot(root);
|
|
123
|
+
try {
|
|
124
|
+
return await run();
|
|
125
|
+
} finally {
|
|
126
|
+
workspaceRootModule.setWorkspaceRoot(previousWorkspaceRoot);
|
|
127
|
+
if (previousEnvWorkspaceRoot === undefined) {
|
|
128
|
+
delete process.env.NX_WORKSPACE_ROOT_PATH;
|
|
129
|
+
} else {
|
|
130
|
+
process.env.NX_WORKSPACE_ROOT_PATH = previousEnvWorkspaceRoot;
|
|
131
|
+
}
|
|
132
|
+
process.chdir(previousCwd);
|
|
133
|
+
}
|
|
134
|
+
}
|