@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,122 @@
|
|
|
1
|
+
import type { ReleasePackageInfo } from './core.js';
|
|
2
|
+
|
|
3
|
+
export interface NpmPublishAuthFailureOptions {
|
|
4
|
+
useBootstrapToken: boolean;
|
|
5
|
+
tokenPresent: boolean;
|
|
6
|
+
repository?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NpmPublishDiagnosticShell {
|
|
10
|
+
publish(): Promise<void>;
|
|
11
|
+
versionExists(): Promise<boolean>;
|
|
12
|
+
log(message: string): void;
|
|
13
|
+
error(message: string): void;
|
|
14
|
+
appendSummary(markdown: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function publishWithAuthDiagnostics(
|
|
18
|
+
pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
|
|
19
|
+
shell: NpmPublishDiagnosticShell,
|
|
20
|
+
options: NpmPublishAuthFailureOptions,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
await shell.publish();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
26
|
+
if (await shell.versionExists()) {
|
|
27
|
+
shell.log(`${packageVersion}: publish result already visible on npm; continuing.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
shell.error(npmPublishAuthFailureMessage(pkg, options));
|
|
31
|
+
await shell.appendSummary(npmPublishAuthFailureMarkdown(pkg, options));
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${packageVersion}: npm publish authentication failed. Run smoo release trust-publisher after the package exists on npm; see the warning banner above for details.`,
|
|
34
|
+
{ cause: error },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function npmPublishAuthFailureMessage(
|
|
40
|
+
pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
|
|
41
|
+
options: NpmPublishAuthFailureOptions,
|
|
42
|
+
): string {
|
|
43
|
+
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
44
|
+
const lines = [
|
|
45
|
+
`::error title=npm publish authentication failed::${packageVersion} could not be published. This usually means npm trusted publishing is not configured for this package/workflow/repo, or the bootstrap NPM_TOKEN is missing/invalid.`,
|
|
46
|
+
'',
|
|
47
|
+
`๐จ npm publish authentication failed for ${packageVersion}`,
|
|
48
|
+
'',
|
|
49
|
+
];
|
|
50
|
+
if (options.useBootstrapToken) {
|
|
51
|
+
lines.push(
|
|
52
|
+
'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
|
|
53
|
+
options.tokenPresent
|
|
54
|
+
? 'NODE_AUTH_TOKEN/NPM_TOKEN is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
|
|
55
|
+
: 'NODE_AUTH_TOKEN/NPM_TOKEN is not set. Add a temporary NPM_TOKEN repository secret and rerun the Publish workflow.',
|
|
56
|
+
'',
|
|
57
|
+
'After the first successful publish, run:',
|
|
58
|
+
' smoo release trust-publisher',
|
|
59
|
+
'',
|
|
60
|
+
'Then remove the temporary bootstrap token path for future releases.',
|
|
61
|
+
);
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lines.push(
|
|
66
|
+
'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
|
|
67
|
+
options.tokenPresent
|
|
68
|
+
? 'NODE_AUTH_TOKEN/NPM_TOKEN is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
|
|
69
|
+
: 'NODE_AUTH_TOKEN/NPM_TOKEN is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.',
|
|
70
|
+
'',
|
|
71
|
+
'Fix:',
|
|
72
|
+
'1. Run locally: smoo release trust-publisher',
|
|
73
|
+
'2. Ensure the npm trusted publisher uses:',
|
|
74
|
+
` repository: ${trustedPublisherRepository(options)}`,
|
|
75
|
+
' workflow: publish.yml',
|
|
76
|
+
'3. Rerun the Publish workflow.',
|
|
77
|
+
'',
|
|
78
|
+
'For first-ever package publishes, add a temporary NPM_TOKEN repository secret, publish once, then run smoo release trust-publisher.',
|
|
79
|
+
);
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function npmPublishAuthFailureMarkdown(
|
|
84
|
+
pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
|
|
85
|
+
options: NpmPublishAuthFailureOptions,
|
|
86
|
+
): string {
|
|
87
|
+
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
88
|
+
const lines = ['## ๐จ npm Publish Authentication Failed', '', `Package: \`${packageVersion}\``, ''];
|
|
89
|
+
if (options.useBootstrapToken) {
|
|
90
|
+
lines.push(
|
|
91
|
+
'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
|
|
92
|
+
'',
|
|
93
|
+
options.tokenPresent
|
|
94
|
+
? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
|
|
95
|
+
: '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set. Add a temporary `NPM_TOKEN` repository secret and rerun the Publish workflow.',
|
|
96
|
+
'',
|
|
97
|
+
'After the first successful publish, run `smoo release trust-publisher`, then remove the temporary bootstrap token path for future releases.',
|
|
98
|
+
);
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lines.push(
|
|
103
|
+
'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
|
|
104
|
+
'',
|
|
105
|
+
options.tokenPresent
|
|
106
|
+
? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
|
|
107
|
+
: '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.',
|
|
108
|
+
'',
|
|
109
|
+
'Fix:',
|
|
110
|
+
'',
|
|
111
|
+
'1. Run locally: `smoo release trust-publisher`',
|
|
112
|
+
`2. Ensure the npm trusted publisher uses repository \`${trustedPublisherRepository(options)}\` and workflow \`publish.yml\``,
|
|
113
|
+
'3. Rerun the Publish workflow.',
|
|
114
|
+
'',
|
|
115
|
+
'For first-ever package publishes, add a temporary `NPM_TOKEN` repository secret, publish once, then run `smoo release trust-publisher`.',
|
|
116
|
+
);
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function trustedPublisherRepository(options: Pick<NpmPublishAuthFailureOptions, 'repository'>): string {
|
|
121
|
+
return options.repository ?? 'the current GitHub repository';
|
|
122
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ReleasePackageInfo, ReleaseTarget } from './core.js';
|
|
2
|
+
import { planPublishActions } from './publish-plan.js';
|
|
3
|
+
|
|
4
|
+
export type ReleaseVersionMode = 'new' | 'none';
|
|
5
|
+
|
|
6
|
+
export interface ReleaseSummary<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
7
|
+
sha: string;
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
packages: Package[];
|
|
10
|
+
pushed: boolean;
|
|
11
|
+
published: Package[];
|
|
12
|
+
alreadyPublished: Package[];
|
|
13
|
+
githubReleases: Package[];
|
|
14
|
+
rerunRequired: boolean;
|
|
15
|
+
noRelease: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ReleaseCompletionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
19
|
+
gitHead(): Promise<string>;
|
|
20
|
+
pushReleaseRefs(packages: Package[]): Promise<boolean>;
|
|
21
|
+
listNpmMissingPackages(packages: Package[]): Promise<Package[]>;
|
|
22
|
+
buildReleaseCandidate(packages: Package[]): Promise<void>;
|
|
23
|
+
publishPackage(pkg: Package, distTag: string, dryRun: boolean): Promise<void>;
|
|
24
|
+
listGithubMissingPackages(packages: Package[]): Promise<Package[]>;
|
|
25
|
+
createGithubRelease(pkg: Package, dryRun: boolean): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
|
|
29
|
+
extends ReleaseCompletionShell<Package> {
|
|
30
|
+
checkout(ref: string): Promise<void>;
|
|
31
|
+
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
32
|
+
beforeRepairTarget?(target: ReleaseTarget<Package>): void;
|
|
33
|
+
afterRepairTarget?(target: ReleaseTarget<Package>): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
37
|
+
releasePackagesAtHead(): Promise<Package[]>;
|
|
38
|
+
ensureLocalReleaseTags(packages: Package[]): Promise<void>;
|
|
39
|
+
gitHead(): Promise<string>;
|
|
40
|
+
runNxReleaseVersion(bump: string, dryRun: boolean): Promise<void>;
|
|
41
|
+
assertCleanGitTree(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ReleaseVersionResult<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
45
|
+
mode: ReleaseVersionMode;
|
|
46
|
+
packages: Package[];
|
|
47
|
+
status: 'already-release-target' | 'dry-run' | 'new-release' | 'no-release-needed';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runReleaseVersion<Package extends ReleasePackageInfo>(
|
|
51
|
+
shell: ReleaseVersionShell<Package>,
|
|
52
|
+
options: { bump: string; dryRun: boolean },
|
|
53
|
+
): Promise<ReleaseVersionResult<Package>> {
|
|
54
|
+
if (!options.dryRun) {
|
|
55
|
+
const localRelease = await shell.releasePackagesAtHead();
|
|
56
|
+
if (localRelease.length > 0) {
|
|
57
|
+
await shell.ensureLocalReleaseTags(localRelease);
|
|
58
|
+
return { mode: 'none', packages: localRelease, status: 'already-release-target' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const headBeforeVersioning = await shell.gitHead();
|
|
63
|
+
await shell.runNxReleaseVersion(options.bump, options.dryRun);
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
return { mode: 'none', packages: [], status: 'dry-run' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await shell.assertCleanGitTree();
|
|
69
|
+
const headAfterVersioning = await shell.gitHead();
|
|
70
|
+
const releasedPackages = await shell.releasePackagesAtHead();
|
|
71
|
+
await shell.ensureLocalReleaseTags(releasedPackages);
|
|
72
|
+
if (headAfterVersioning === headBeforeVersioning) {
|
|
73
|
+
if (releasedPackages.length > 0) {
|
|
74
|
+
return { mode: 'none', packages: releasedPackages, status: 'already-release-target' };
|
|
75
|
+
}
|
|
76
|
+
if (options.bump !== 'auto') {
|
|
77
|
+
throw new Error(`Nx did not create a release commit for forced --bump ${options.bump}.`);
|
|
78
|
+
}
|
|
79
|
+
return { mode: 'none', packages: [], status: 'no-release-needed' };
|
|
80
|
+
}
|
|
81
|
+
if (releasedPackages.length === 0) {
|
|
82
|
+
throw new Error('Nx created a release commit without current package release tags at HEAD.');
|
|
83
|
+
}
|
|
84
|
+
return { mode: 'new', packages: releasedPackages, status: 'new-release' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function completeReleaseAtHead<Package extends ReleasePackageInfo>(
|
|
88
|
+
shell: ReleaseCompletionShell<Package>,
|
|
89
|
+
packages: Package[],
|
|
90
|
+
dryRun: boolean,
|
|
91
|
+
rerunRequired: boolean,
|
|
92
|
+
): Promise<ReleaseSummary<Package>> {
|
|
93
|
+
const npmMissingPackages = await shell.listNpmMissingPackages(packages);
|
|
94
|
+
const githubMissingPackages = dryRun ? packages : await shell.listGithubMissingPackages(packages);
|
|
95
|
+
return completePlannedRelease(shell, {
|
|
96
|
+
sha: await shell.gitHead(),
|
|
97
|
+
dryRun,
|
|
98
|
+
packages,
|
|
99
|
+
pushed: dryRun ? false : await shell.pushReleaseRefs(packages),
|
|
100
|
+
npmMissingPackages,
|
|
101
|
+
githubMissingPackages,
|
|
102
|
+
rerunRequired,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function repairPendingTargets<Package extends ReleasePackageInfo>(
|
|
107
|
+
shell: ReleaseRepairShell<Package>,
|
|
108
|
+
targets: Array<ReleaseTarget<Package>>,
|
|
109
|
+
restoreRef: string,
|
|
110
|
+
dryRun: boolean,
|
|
111
|
+
): Promise<Array<ReleaseSummary<Package>>> {
|
|
112
|
+
const summaries: Array<ReleaseSummary<Package>> = [];
|
|
113
|
+
try {
|
|
114
|
+
for (const target of targets) {
|
|
115
|
+
await shell.checkout(target.sha);
|
|
116
|
+
if (target.packages.length === 0) {
|
|
117
|
+
throw new Error(`Release commit ${target.sha} has no release packages after checkout.`);
|
|
118
|
+
}
|
|
119
|
+
shell.beforeRepairTarget?.(target);
|
|
120
|
+
try {
|
|
121
|
+
summaries.push(await shell.withDevenvEnv(() => completeRepairTargetAtHead(shell, target, dryRun)));
|
|
122
|
+
} finally {
|
|
123
|
+
shell.afterRepairTarget?.(target);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
await shell.checkout(restoreRef);
|
|
128
|
+
}
|
|
129
|
+
return summaries;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function completeRepairTargetAtHead<Package extends ReleasePackageInfo>(
|
|
133
|
+
shell: ReleaseCompletionShell<Package>,
|
|
134
|
+
target: ReleaseTarget<Package>,
|
|
135
|
+
dryRun: boolean,
|
|
136
|
+
): Promise<ReleaseSummary<Package>> {
|
|
137
|
+
return completePlannedRelease(shell, {
|
|
138
|
+
sha: await shell.gitHead(),
|
|
139
|
+
dryRun,
|
|
140
|
+
packages: target.packages,
|
|
141
|
+
pushed: dryRun ? false : await shell.pushReleaseRefs(target.packages),
|
|
142
|
+
npmMissingPackages: target.npmPackages,
|
|
143
|
+
githubMissingPackages: target.githubPackages,
|
|
144
|
+
rerunRequired: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function completePlannedRelease<Package extends ReleasePackageInfo>(
|
|
149
|
+
shell: ReleaseCompletionShell<Package>,
|
|
150
|
+
input: {
|
|
151
|
+
sha: string;
|
|
152
|
+
dryRun: boolean;
|
|
153
|
+
packages: Package[];
|
|
154
|
+
pushed: boolean;
|
|
155
|
+
npmMissingPackages: Package[];
|
|
156
|
+
githubMissingPackages: Package[];
|
|
157
|
+
rerunRequired: boolean;
|
|
158
|
+
},
|
|
159
|
+
): Promise<ReleaseSummary<Package>> {
|
|
160
|
+
const plan = planPublishActions({
|
|
161
|
+
releasePackages: input.packages,
|
|
162
|
+
npmMissingPackages: input.npmMissingPackages,
|
|
163
|
+
githubMissingPackages: input.githubMissingPackages,
|
|
164
|
+
});
|
|
165
|
+
if (plan.buildProjects.length > 0) {
|
|
166
|
+
await shell.buildReleaseCandidate(plan.publishPackages.map((action) => action.pkg));
|
|
167
|
+
}
|
|
168
|
+
for (const { pkg, distTag } of plan.publishPackages) {
|
|
169
|
+
await shell.publishPackage(pkg, distTag, input.dryRun);
|
|
170
|
+
}
|
|
171
|
+
for (const pkg of plan.githubReleasePackages) {
|
|
172
|
+
await shell.createGithubRelease(pkg, input.dryRun);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
sha: input.sha,
|
|
177
|
+
dryRun: input.dryRun,
|
|
178
|
+
packages: input.packages,
|
|
179
|
+
pushed: input.pushed,
|
|
180
|
+
published: input.dryRun ? [] : plan.publishPackages.map((action) => action.pkg),
|
|
181
|
+
alreadyPublished: input.packages.filter((pkg) => !input.npmMissingPackages.includes(pkg)),
|
|
182
|
+
githubReleases: input.dryRun ? [] : plan.githubReleasePackages,
|
|
183
|
+
rerunRequired: input.rerunRequired,
|
|
184
|
+
noRelease: false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { npmDistTagForVersion, type ReleasePackageInfo } from './core.js';
|
|
2
|
+
|
|
3
|
+
export interface PublishPackageAction<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
4
|
+
pkg: Package;
|
|
5
|
+
distTag: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PublishPlan<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
9
|
+
buildProjects: string[];
|
|
10
|
+
publishPackages: Array<PublishPackageAction<Package>>;
|
|
11
|
+
githubReleasePackages: Package[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PublishPlanInput<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
15
|
+
releasePackages: Package[];
|
|
16
|
+
npmMissingPackages: Package[];
|
|
17
|
+
githubMissingPackages: Package[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function planPublishActions<Package extends ReleasePackageInfo>(
|
|
21
|
+
input: PublishPlanInput<Package>,
|
|
22
|
+
): PublishPlan<Package> {
|
|
23
|
+
const npmMissingNames = packageNameSet(input.npmMissingPackages);
|
|
24
|
+
const githubMissingNames = packageNameSet(input.githubMissingPackages);
|
|
25
|
+
const npmPackages = input.releasePackages.filter((pkg) => npmMissingNames.has(pkg.name));
|
|
26
|
+
const githubReleasePackages = input.releasePackages.filter((pkg) => githubMissingNames.has(pkg.name));
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
buildProjects: npmPackages.map((pkg) => pkg.name),
|
|
30
|
+
publishPackages: npmPackages.map((pkg) => ({ pkg, distTag: npmDistTagForVersion(pkg.version) })),
|
|
31
|
+
githubReleasePackages,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function packageNameSet(packages: ReleasePackageInfo[]): Set<string> {
|
|
36
|
+
return new Set(packages.map((pkg) => pkg.name));
|
|
37
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type ReleasePackageInfo, releasePackageForTag } from './core.js';
|
|
2
|
+
|
|
3
|
+
export interface RetagUnpublishedOptions {
|
|
4
|
+
tags: string[];
|
|
5
|
+
toRef: string;
|
|
6
|
+
push: boolean;
|
|
7
|
+
dispatch: boolean;
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
branch: string;
|
|
10
|
+
workflow: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RetagUnpublishedTagUpdate<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
14
|
+
tag: string;
|
|
15
|
+
pkg: Package;
|
|
16
|
+
expectedRemoteObject: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RetagUnpublishedShell<Package extends Omit<ReleasePackageInfo, 'version'> = ReleasePackageInfo> {
|
|
20
|
+
listReleasePackages(): Package[];
|
|
21
|
+
resolveRef(ref: string): Promise<string>;
|
|
22
|
+
resolveDispatchRef(branch: string): Promise<string | null>;
|
|
23
|
+
packageVersionAtRef(packagePath: string, ref: string): Promise<string | null>;
|
|
24
|
+
npmVersionExists(name: string, version: string): Promise<boolean>;
|
|
25
|
+
githubReleaseExists(tag: string): Promise<boolean>;
|
|
26
|
+
remoteTagObject(tag: string): Promise<string | null>;
|
|
27
|
+
createOrMoveTag(tag: string, ref: string): Promise<void>;
|
|
28
|
+
pushTags(updates: Array<RetagUnpublishedTagUpdate<Package & { version: string }>>): Promise<void>;
|
|
29
|
+
dispatchPublishWorkflow(workflow: string, branch: string): Promise<void>;
|
|
30
|
+
log(message: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function retagUnpublished<Package extends Omit<ReleasePackageInfo, 'version'>>(
|
|
34
|
+
shell: RetagUnpublishedShell<Package>,
|
|
35
|
+
options: RetagUnpublishedOptions,
|
|
36
|
+
): Promise<Array<RetagUnpublishedTagUpdate<Package & { version: string }>>> {
|
|
37
|
+
if (options.tags.length === 0) {
|
|
38
|
+
throw new Error('retag-unpublished requires at least one release tag.');
|
|
39
|
+
}
|
|
40
|
+
assertUniqueTags(options.tags);
|
|
41
|
+
|
|
42
|
+
const targetSha = await shell.resolveRef(options.toRef);
|
|
43
|
+
if (options.dispatch) {
|
|
44
|
+
const dispatchSha = await shell.resolveDispatchRef(options.branch);
|
|
45
|
+
if (!dispatchSha) {
|
|
46
|
+
throw new Error(`Cannot dispatch ${options.workflow}: remote branch ${options.branch} was not found.`);
|
|
47
|
+
}
|
|
48
|
+
if (dispatchSha !== targetSha) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Cannot dispatch ${options.workflow}: ${options.toRef} resolves to ${targetSha.slice(0, 12)}, but ${
|
|
51
|
+
options.branch
|
|
52
|
+
} resolves to ${dispatchSha.slice(0, 12)}. Push the branch first or choose a ref already at the remote branch HEAD.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const packages = shell.listReleasePackages();
|
|
58
|
+
const updates: Array<RetagUnpublishedTagUpdate<Package & { version: string }>> = [];
|
|
59
|
+
for (const tag of options.tags) {
|
|
60
|
+
const match = releasePackageForTag(packages, tag);
|
|
61
|
+
if (!match) {
|
|
62
|
+
throw new Error(`Release tag ${tag} does not match an owned release package.`);
|
|
63
|
+
}
|
|
64
|
+
const versionAtRef = await shell.packageVersionAtRef(match.pkg.path, options.toRef);
|
|
65
|
+
if (versionAtRef !== match.version) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Release tag ${tag} cannot move to ${options.toRef}: ${match.pkg.path}/package.json has version ${
|
|
68
|
+
versionAtRef ?? 'missing'
|
|
69
|
+
}, expected ${match.version}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (await shell.npmVersionExists(match.pkg.name, match.version)) {
|
|
73
|
+
throw new Error(`Cannot retag ${tag}: ${match.pkg.name}@${match.version} already exists on npm.`);
|
|
74
|
+
}
|
|
75
|
+
if (await shell.githubReleaseExists(tag)) {
|
|
76
|
+
throw new Error(`Cannot retag ${tag}: GitHub Release ${tag} already exists.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updates.push({
|
|
80
|
+
tag,
|
|
81
|
+
pkg: { ...match.pkg, version: match.version },
|
|
82
|
+
expectedRemoteObject: options.push ? await shell.remoteTagObject(tag) : null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const update of updates) {
|
|
87
|
+
if (options.dryRun) {
|
|
88
|
+
shell.log(`Would move ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
|
|
89
|
+
} else {
|
|
90
|
+
await shell.createOrMoveTag(update.tag, options.toRef);
|
|
91
|
+
shell.log(`Moved ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (options.push) {
|
|
95
|
+
if (options.dryRun) {
|
|
96
|
+
shell.log(`Would push ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
|
|
97
|
+
} else {
|
|
98
|
+
await shell.pushTags(updates);
|
|
99
|
+
shell.log(`Pushed ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (options.dispatch) {
|
|
103
|
+
if (options.dryRun) {
|
|
104
|
+
shell.log(`Would dispatch ${options.workflow} on ${options.branch} with bump=auto.`);
|
|
105
|
+
} else {
|
|
106
|
+
await shell.dispatchPublishWorkflow(options.workflow, options.branch);
|
|
107
|
+
shell.log(`Dispatched ${options.workflow} on ${options.branch} with bump=auto.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return updates;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function assertUniqueTags(tags: string[]): void {
|
|
115
|
+
const seen = new Set<string>();
|
|
116
|
+
for (const tag of tags) {
|
|
117
|
+
if (seen.has(tag)) {
|
|
118
|
+
throw new Error(`Release tag ${tag} was provided more than once.`);
|
|
119
|
+
}
|
|
120
|
+
seen.add(tag);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
bump:
|
|
7
|
-
type: choice
|
|
8
|
-
description:
|
|
9
|
-
Use auto for conventional commits, or force a semver bump. Prerelease publishes to next; all others publish to
|
|
10
|
-
latest.
|
|
11
|
-
options: [auto, patch, minor, major, prerelease]
|
|
12
|
-
default: auto
|
|
13
|
-
dry_run:
|
|
14
|
-
type: boolean
|
|
15
|
-
description: Run release commands without writing versions, tags, publishes, or GitHub Releases.
|
|
16
|
-
default: false
|
|
17
|
-
|
|
18
|
-
permissions:
|
|
19
|
-
contents: write
|
|
20
|
-
id-token: write
|
|
21
|
-
|
|
22
|
-
concurrency:
|
|
23
|
-
group: release-${{ github.ref }}
|
|
24
|
-
cancel-in-progress: false
|
|
25
|
-
|
|
26
|
-
defaults:
|
|
27
|
-
run:
|
|
28
|
-
working-directory: tooling/direnv
|
|
29
|
-
|
|
30
|
-
jobs:
|
|
31
|
-
publish:
|
|
32
|
-
runs-on: ubuntu-latest
|
|
33
|
-
env:
|
|
34
|
-
NIX_STORE_NAR: ${{ github.workspace }}/nix-store.nar
|
|
35
|
-
GH_TOKEN: ${{ github.token }}
|
|
36
|
-
steps:
|
|
37
|
-
# --- Setup --------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
# Step 1: GitHub adds "Set up job" automatically
|
|
40
|
-
# Step 2
|
|
41
|
-
- name: ๐ฅ Checkout
|
|
42
|
-
uses: actions/checkout@v6.0.2
|
|
43
|
-
with:
|
|
44
|
-
filter: blob:none
|
|
45
|
-
fetch-depth: 0
|
|
46
|
-
|
|
47
|
-
# Step 3. Composite action internals do not affect top-level job step
|
|
48
|
-
# anchors; update these comments if top-level steps move.
|
|
49
|
-
- name: ๐งฑ Setup Nix/devenv
|
|
50
|
-
id: setup
|
|
51
|
-
uses: ./.github/actions/setup-devenv
|
|
52
|
-
|
|
53
|
-
# --- Validation ---------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
# Release validation intentionally does not restore persisted Nx task
|
|
56
|
-
# cache. Nx may reuse tasks produced earlier in this same job, but publish
|
|
57
|
-
# never relies on task outputs restored from CI cache.
|
|
58
|
-
|
|
59
|
-
# Step 4
|
|
60
|
-
- name: โ
Check managed monorepo files
|
|
61
|
-
run: smoo monorepo check
|
|
62
|
-
|
|
63
|
-
# Step 5
|
|
64
|
-
- name: ๐ฆ Resolve release projects
|
|
65
|
-
id: projects
|
|
66
|
-
run: echo "projects=$(smoo monorepo list-public-projects)" >> "$GITHUB_OUTPUT"
|
|
67
|
-
|
|
68
|
-
# Step 6
|
|
69
|
-
- name: ๐จ Build
|
|
70
|
-
run: smoo github-ci nx-run-many --targets build --projects "${{ steps.projects.outputs.projects }}"
|
|
71
|
-
|
|
72
|
-
# Step 7
|
|
73
|
-
- name: ๐ Lint
|
|
74
|
-
run: smoo github-ci nx-run-many --targets lint --projects "${{ steps.projects.outputs.projects }}"
|
|
75
|
-
|
|
76
|
-
# Step 8
|
|
77
|
-
- name: ๐งช Unit Tests
|
|
78
|
-
run: smoo github-ci nx-run-many --targets test --projects "${{ steps.projects.outputs.projects }}"
|
|
79
|
-
|
|
80
|
-
# Step 9
|
|
81
|
-
- name: ๐ Upload trace DBs
|
|
82
|
-
if: ${{ failure() }}
|
|
83
|
-
uses: actions/upload-artifact@v4
|
|
84
|
-
with:
|
|
85
|
-
name: trace-results-${{ github.run_id }}
|
|
86
|
-
path: packages/*/.trace-results.db
|
|
87
|
-
if-no-files-found: ignore
|
|
88
|
-
retention-days: 14
|
|
89
|
-
include-hidden-files: true
|
|
90
|
-
|
|
91
|
-
# Step 10
|
|
92
|
-
- name: โ
Validate monorepo config
|
|
93
|
-
run: smoo monorepo validate
|
|
94
|
-
|
|
95
|
-
# --- Release ------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
# Step 11
|
|
98
|
-
- name: ๐ค Configure release author
|
|
99
|
-
run:
|
|
100
|
-
git config user.name "github-actions[bot]" && git config user.email
|
|
101
|
-
"41898282+github-actions[bot]@users.noreply.github.com"
|
|
102
|
-
|
|
103
|
-
# Step 12
|
|
104
|
-
- name: ๐ท๏ธ Version, tag, and push packages
|
|
105
|
-
run: smoo release version --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"
|
|
106
|
-
|
|
107
|
-
# Step 13
|
|
108
|
-
- name: ๐ Collect release tags
|
|
109
|
-
id: tags
|
|
110
|
-
run: echo "tags=$(git tag --points-at HEAD | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
|
|
111
|
-
|
|
112
|
-
# Step 14
|
|
113
|
-
- name: ๐ฆ Publish packages
|
|
114
|
-
# smoo packs with Bun, then publishes tarballs with npm. npm uses OIDC
|
|
115
|
-
# trusted publishing when configured; NODE_AUTH_TOKEN is only a temporary
|
|
116
|
-
# bootstrap fallback before packages have trusted publishers.
|
|
117
|
-
env:
|
|
118
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
119
|
-
run: smoo release publish --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"
|
|
120
|
-
|
|
121
|
-
# Step 15
|
|
122
|
-
- name: ๐ Create or update GitHub Releases
|
|
123
|
-
if: ${{ inputs.dry_run != true }}
|
|
124
|
-
run:
|
|
125
|
-
smoo release github-release --tags "${{ steps.tags.outputs.tags }}" --bump "${{ inputs.bump }}" --dry-run "${{
|
|
126
|
-
inputs.dry_run }}"
|
|
127
|
-
|
|
128
|
-
# --- Cleanup ------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
# Step 16
|
|
131
|
-
- name: ๐งน Cleanup and cache Nix/devenv
|
|
132
|
-
if: ${{ always() }}
|
|
133
|
-
uses: ./.github/actions/save-nix-devenv
|
|
134
|
-
with:
|
|
135
|
-
nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}
|
|
136
|
-
devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}
|