@smoothbricks/cli 0.1.0 → 0.2.0
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 +338 -88
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +136 -22
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- 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/run.d.ts +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +20 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +87 -7
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +24 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +136 -15
- 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 +28 -11
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +25 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1289 -27
- 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 +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +69 -31
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +240 -18
- package/dist/monorepo/publish-workflow.d.ts +82 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -0
- package/dist/monorepo/publish-workflow.js +336 -0
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -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/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +51 -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 +39 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +763 -130
- package/dist/release/npm-auth.d.ts +16 -0
- package/dist/release/npm-auth.d.ts.map +1 -0
- package/dist/release/npm-auth.js +39 -0
- package/dist/release/orchestration.d.ts +49 -0
- package/dist/release/orchestration.d.ts.map +1 -0
- package/dist/release/orchestration.js +113 -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/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +3 -0
- package/package.json +37 -4
- package/src/cli.ts +193 -40
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/devenv.test.ts +28 -0
- package/src/lib/devenv.ts +89 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +117 -7
- package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +322 -0
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +181 -15
- package/src/monorepo/lockfile.ts +52 -7
- package/src/monorepo/managed-files.ts +43 -13
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +902 -0
- package/src/monorepo/package-policy.ts +1527 -25
- package/src/monorepo/packed-manifest.ts +67 -0
- package/src/monorepo/packed-package.ts +90 -31
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +314 -19
- package/src/monorepo/publish-workflow.ts +422 -0
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +64 -0
- package/src/nx/index.ts +119 -0
- package/src/nx-version-actions.cts +36 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +149 -0
- package/src/release/__tests__/core-scenarios.test.ts +187 -0
- package/src/release/__tests__/core.test.ts +73 -0
- package/src/release/__tests__/fixture-repo.test.ts +314 -0
- package/src/release/__tests__/github-release.test.ts +149 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +114 -0
- package/src/release/__tests__/npm-auth.test.ts +120 -0
- package/src/release/__tests__/orchestration.test.ts +246 -0
- package/src/release/__tests__/publish-plan.test.ts +67 -0
- package/src/release/__tests__/retag-unpublished.test.ts +160 -0
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +171 -0
- package/src/release/github-release.ts +134 -0
- package/src/release/index.ts +974 -138
- package/src/release/npm-auth.ts +93 -0
- package/src/release/orchestration.ts +196 -0
- package/src/release/publish-plan.ts +37 -0
- package/src/release/retag-unpublished.ts +122 -0
- package/managed/raw/.git-format-staged.yml +0 -47
- package/managed/templates/github/workflows/publish.yml +0 -136
package/src/release/index.ts
CHANGED
|
@@ -1,142 +1,318 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
6
|
import { $ } from 'bun';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { withDevenvEnv } from '../lib/devenv.js';
|
|
8
|
+
import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
|
|
9
|
+
import { decode, run, runResult, runStatus } from '../lib/run.js';
|
|
10
|
+
import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
|
|
11
|
+
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
12
|
+
import {
|
|
13
|
+
type BootstrapNpmPackagesOptions,
|
|
14
|
+
bootstrapNpmPackages,
|
|
15
|
+
NPM_BOOTSTRAP_DIST_TAG,
|
|
16
|
+
NPM_BOOTSTRAP_VERSION,
|
|
17
|
+
} from './bootstrap-npm-packages.js';
|
|
18
|
+
import { autoReleaseCandidatePackages } from './candidates.js';
|
|
19
|
+
import {
|
|
20
|
+
type ReleaseTagRecord as CoreReleaseTagRecord,
|
|
21
|
+
type ReleaseTarget as CoreReleaseTarget,
|
|
22
|
+
collectOwnedReleaseTagRecords,
|
|
23
|
+
type GitReleaseTagInfo,
|
|
24
|
+
pendingReleaseTargets,
|
|
25
|
+
type ReleasePackageInfo,
|
|
26
|
+
releaseTag,
|
|
27
|
+
} from './core.js';
|
|
28
|
+
import { createOrUpdateGithubRelease, renderNxProjectChangelogContents } from './github-release.js';
|
|
29
|
+
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
30
|
+
import {
|
|
31
|
+
completeReleaseAtHead as completeReleaseAtHeadWithShell,
|
|
32
|
+
type ReleaseCompletionShell,
|
|
33
|
+
type ReleaseSummary,
|
|
34
|
+
type ReleaseVersionMode,
|
|
35
|
+
repairPendingTargets,
|
|
36
|
+
runReleaseVersion,
|
|
37
|
+
} from './orchestration.js';
|
|
38
|
+
import { type RetagUnpublishedTagUpdate, retagUnpublished } from './retag-unpublished.js';
|
|
9
39
|
|
|
10
40
|
export interface ReleaseVersionOptions {
|
|
11
41
|
bump: string;
|
|
12
42
|
dryRun?: boolean;
|
|
43
|
+
githubOutput?: string;
|
|
13
44
|
}
|
|
14
45
|
|
|
15
46
|
export interface ReleasePublishOptions {
|
|
16
47
|
bump: string;
|
|
17
|
-
tag?: string;
|
|
18
|
-
npmTag?: string;
|
|
19
48
|
dryRun?: boolean;
|
|
20
49
|
}
|
|
21
50
|
|
|
22
|
-
export interface
|
|
23
|
-
|
|
51
|
+
export interface ReleaseRepairPendingOptions {
|
|
52
|
+
dryRun?: boolean;
|
|
24
53
|
}
|
|
25
54
|
|
|
26
55
|
export interface ReleaseTrustPublisherOptions {
|
|
27
56
|
dryRun?: boolean;
|
|
57
|
+
bootstrap?: boolean;
|
|
28
58
|
otp?: string;
|
|
59
|
+
bootstrapOtp?: string;
|
|
29
60
|
skipLogin?: boolean;
|
|
30
61
|
}
|
|
31
62
|
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (state.allPublished) {
|
|
39
|
-
console.log('Current package versions are already published; skipping version bump.');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
63
|
+
export interface ReleaseBootstrapNpmPackagesOptions {
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
skipLogin?: boolean;
|
|
66
|
+
otp?: string;
|
|
67
|
+
packages?: string[];
|
|
68
|
+
}
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
export interface ReleaseRetagUnpublishedOptions {
|
|
71
|
+
tags: string[];
|
|
72
|
+
to?: string;
|
|
73
|
+
push?: boolean;
|
|
74
|
+
dispatch?: boolean;
|
|
75
|
+
dryRun?: boolean;
|
|
76
|
+
remote?: string;
|
|
77
|
+
branch?: string;
|
|
78
|
+
}
|
|
44
79
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
await pushReleaseCommit(root);
|
|
80
|
+
export async function releaseVersion(root: string, options: ReleaseVersionOptions): Promise<void> {
|
|
81
|
+
const bump = releaseBumpArg(options.bump);
|
|
82
|
+
const packages = releasePackages(root);
|
|
83
|
+
const result = await runReleaseVersion(
|
|
84
|
+
{
|
|
85
|
+
releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
|
|
86
|
+
releaseVersionPackages: (releaseBump) => releaseVersionPackages(root, packages, releaseBump),
|
|
87
|
+
ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
|
|
88
|
+
gitHead: () => gitHead(root),
|
|
89
|
+
runNxReleaseVersion: (releasePackages, releaseBump, dryRun) =>
|
|
90
|
+
runNxReleaseVersion(root, releasePackageProjects(releasePackages), releaseBump, dryRun),
|
|
91
|
+
assertCleanGitTree: () => assertCleanGitTree(root),
|
|
92
|
+
},
|
|
93
|
+
{ bump, dryRun: options.dryRun === true },
|
|
94
|
+
);
|
|
95
|
+
if (result.status === 'already-release-target') {
|
|
96
|
+
console.log('HEAD is already a release target; publish will complete any missing durable state.');
|
|
97
|
+
} else if (result.status === 'no-release-needed') {
|
|
98
|
+
console.log('Nx did not create a release commit; no release needed.');
|
|
67
99
|
}
|
|
100
|
+
await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
export async function releasePublish(root: string, options: ReleasePublishOptions): Promise<void> {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
const bump = releaseBumpArg(options.bump);
|
|
105
|
+
const packages = await releasePackagesAtHead(root, releasePackages(root));
|
|
106
|
+
if (packages.length === 0) {
|
|
107
|
+
if (bump === 'auto') {
|
|
108
|
+
console.log('No release tags found at HEAD; no release to publish.');
|
|
109
|
+
const summary: ReleaseSummary<ReleasePackage> = {
|
|
110
|
+
sha: await gitHead(root),
|
|
111
|
+
dryRun: options.dryRun === true,
|
|
112
|
+
packages,
|
|
113
|
+
pushed: false,
|
|
114
|
+
published: [],
|
|
115
|
+
alreadyPublished: [],
|
|
116
|
+
githubReleases: [],
|
|
117
|
+
rerunRequired: false,
|
|
118
|
+
noRelease: true,
|
|
119
|
+
};
|
|
120
|
+
await writeReleaseSummary(summary);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw new Error('No release tags found at HEAD for current package versions. Run smoo release version first.');
|
|
81
124
|
}
|
|
125
|
+
const summary = await completeReleaseAtHeadWithShell(
|
|
126
|
+
releaseCompletionShell(root),
|
|
127
|
+
packages,
|
|
128
|
+
options.dryRun === true,
|
|
129
|
+
await newerCommitsRemain(root),
|
|
130
|
+
);
|
|
131
|
+
await writeReleaseSummary(summary);
|
|
82
132
|
}
|
|
83
133
|
|
|
84
|
-
export async function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await run('gh', ['release', 'create', tag, '--title', tag, '--generate-notes', `--latest=${latestFlag}`], root);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
134
|
+
export async function releaseRepairPending(root: string, options: ReleaseRepairPendingOptions): Promise<void> {
|
|
135
|
+
const branch = await releaseBranch(root);
|
|
136
|
+
const remote = await releaseRemote(root, branch);
|
|
137
|
+
console.log(`Repair pending releases: fetching ${remote}/${branch} and tags.`);
|
|
138
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
139
|
+
const remoteRef = `${remote}/${branch}`;
|
|
140
|
+
const restoreRef = (await gitRefExists(root, remoteRef)) ? remoteRef : await gitHead(root);
|
|
141
|
+
console.log(`Repair pending releases: planning from ${restoreRef}.`);
|
|
142
|
+
const targets = await listPendingReleaseTargets(root, restoreRef);
|
|
143
|
+
console.log(
|
|
144
|
+
targets.length === 0
|
|
145
|
+
? 'Repair pending releases: no pending durable state repairs found.'
|
|
146
|
+
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
|
|
147
|
+
);
|
|
148
|
+
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
149
|
+
await writeRepairSummary(summaries, options.dryRun === true);
|
|
103
150
|
}
|
|
104
151
|
|
|
105
152
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
106
153
|
const repository = githubRepositoryFromRootPackage(root);
|
|
107
154
|
const workflow = 'publish.yml';
|
|
108
|
-
|
|
155
|
+
await configureTrustedPublishers(
|
|
156
|
+
{
|
|
157
|
+
repository,
|
|
158
|
+
workflow,
|
|
159
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
160
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
161
|
+
bootstrapNpmPackages: (bootstrapOptions) =>
|
|
162
|
+
bootstrapNpmPackages(
|
|
163
|
+
{
|
|
164
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
165
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
166
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
167
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
168
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
169
|
+
log: (message) => console.log(message),
|
|
170
|
+
},
|
|
171
|
+
bootstrapOptions,
|
|
172
|
+
),
|
|
173
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
174
|
+
trustPublisher: (pkg, dryRun, env) => {
|
|
175
|
+
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
176
|
+
if (dryRun) {
|
|
177
|
+
args.push('--dry-run');
|
|
178
|
+
}
|
|
179
|
+
return runLatestNpmTrust(root, args, env);
|
|
180
|
+
},
|
|
181
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
182
|
+
log: (message) => console.log(message),
|
|
183
|
+
error: (message) => console.error(message),
|
|
184
|
+
},
|
|
185
|
+
options,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface TrustPublisherShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
190
|
+
repository: string;
|
|
191
|
+
workflow: string;
|
|
192
|
+
listReleasePackages(): Package[];
|
|
193
|
+
packageExists(name: string): Promise<boolean>;
|
|
194
|
+
bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
|
|
195
|
+
login(): Promise<void>;
|
|
196
|
+
trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
|
|
197
|
+
promptOtp(packageName: string): Promise<string>;
|
|
198
|
+
log(message: string): void;
|
|
199
|
+
error(message: string): void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type TrustPublisherResult = 'configured' | 'already-configured';
|
|
203
|
+
|
|
204
|
+
export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
|
|
205
|
+
shell: TrustPublisherShell<Package>,
|
|
206
|
+
options: ReleaseTrustPublisherOptions,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const packages = shell.listReleasePackages();
|
|
109
209
|
if (packages.length === 0) {
|
|
110
|
-
throw new Error('No
|
|
210
|
+
throw new Error('No owned release packages found.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let bootstrapLoggedIn = false;
|
|
214
|
+
if (options.bootstrap) {
|
|
215
|
+
const bootstrapped = await shell.bootstrapNpmPackages({
|
|
216
|
+
dryRun: options.dryRun === true,
|
|
217
|
+
skipLogin: options.skipLogin === true,
|
|
218
|
+
otp: options.bootstrapOtp,
|
|
219
|
+
packages: [],
|
|
220
|
+
});
|
|
221
|
+
bootstrapLoggedIn = bootstrapped.length > 0 && !options.dryRun && !options.skipLogin;
|
|
111
222
|
}
|
|
112
223
|
|
|
113
224
|
if (!options.dryRun) {
|
|
114
225
|
const packageStates = await Promise.all(
|
|
115
|
-
packages.map(async (pkg) => ((await
|
|
226
|
+
packages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)),
|
|
116
227
|
);
|
|
117
228
|
const missingPackages = packageStates.filter((name): name is string => name !== null);
|
|
118
229
|
if (missingPackages.length > 0) {
|
|
119
230
|
throw new Error(
|
|
120
231
|
'npm trusted publishing can only be configured after packages exist on the registry. ' +
|
|
121
|
-
'
|
|
232
|
+
'Run smoo release trust-publisher --bootstrap locally. ' +
|
|
122
233
|
`Missing packages: ${missingPackages.join(', ')}`,
|
|
123
234
|
);
|
|
124
235
|
}
|
|
125
236
|
}
|
|
126
237
|
|
|
127
|
-
if (!options.dryRun && !options.skipLogin) {
|
|
128
|
-
await
|
|
238
|
+
if (!options.dryRun && !options.skipLogin && !bootstrapLoggedIn) {
|
|
239
|
+
await shell.login();
|
|
129
240
|
}
|
|
130
241
|
|
|
242
|
+
const failedPackages: string[] = [];
|
|
131
243
|
for (const pkg of packages) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
244
|
+
shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
|
|
245
|
+
while (true) {
|
|
246
|
+
const otp = options.dryRun ? undefined : (options.otp ?? (await shell.promptOtp(pkg.name)));
|
|
247
|
+
try {
|
|
248
|
+
const result = await shell.trustPublisher(
|
|
249
|
+
pkg,
|
|
250
|
+
options.dryRun === true,
|
|
251
|
+
otp ? { NPM_CONFIG_OTP: otp } : undefined,
|
|
252
|
+
);
|
|
253
|
+
if (result === 'already-configured') {
|
|
254
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
259
|
+
shell.error(error instanceof Error ? error.message : String(error));
|
|
260
|
+
if (!options.dryRun) {
|
|
261
|
+
shell.error('npm OTP codes are single-use. Generate a fresh OTP before retrying this package.');
|
|
262
|
+
}
|
|
263
|
+
if (options.otp || options.dryRun) {
|
|
264
|
+
failedPackages.push(pkg.name);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
shell.error('Retrying this package. Press Ctrl-C to stop.');
|
|
268
|
+
}
|
|
136
269
|
}
|
|
137
|
-
const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
|
|
138
|
-
await runLatestNpm(root, args, otp ? { NPM_CONFIG_OTP: otp } : undefined);
|
|
139
270
|
}
|
|
271
|
+
if (failedPackages.length > 0) {
|
|
272
|
+
shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
273
|
+
if (options.otp) {
|
|
274
|
+
shell.error('Rerun smoo release trust-publisher without --otp to enter a fresh OTP for each failed package.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function releaseBootstrapNpmPackages(
|
|
280
|
+
root: string,
|
|
281
|
+
options: ReleaseBootstrapNpmPackagesOptions,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
await bootstrapNpmPackages(
|
|
284
|
+
{
|
|
285
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
286
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
287
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
288
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
289
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
290
|
+
log: (message) => console.log(message),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
dryRun: options.dryRun === true,
|
|
294
|
+
skipLogin: options.skipLogin === true,
|
|
295
|
+
otp: options.otp,
|
|
296
|
+
packages: options.packages ?? [],
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function releaseRetagUnpublished(root: string, options: ReleaseRetagUnpublishedOptions): Promise<void> {
|
|
302
|
+
const toRef = options.to ?? 'HEAD';
|
|
303
|
+
const dispatch = options.dispatch === true;
|
|
304
|
+
const push = options.push === true || dispatch;
|
|
305
|
+
const branch = options.branch ?? (await releaseBranch(root));
|
|
306
|
+
const remote = options.remote ?? (push ? await releaseRemote(root, branch) : 'origin');
|
|
307
|
+
await retagUnpublished(releaseRetagShell(root, remote), {
|
|
308
|
+
tags: options.tags,
|
|
309
|
+
toRef,
|
|
310
|
+
push,
|
|
311
|
+
dispatch,
|
|
312
|
+
dryRun: options.dryRun === true,
|
|
313
|
+
branch,
|
|
314
|
+
workflow: 'publish.yml',
|
|
315
|
+
});
|
|
140
316
|
}
|
|
141
317
|
|
|
142
318
|
export async function printReleaseState(root: string): Promise<void> {
|
|
@@ -148,43 +324,101 @@ interface ReleaseState {
|
|
|
148
324
|
allPublished: boolean;
|
|
149
325
|
}
|
|
150
326
|
|
|
151
|
-
type
|
|
327
|
+
type ReleasePackage = ReturnType<typeof listReleasePackages>[number];
|
|
328
|
+
|
|
329
|
+
type ReleaseTarget = CoreReleaseTarget<ReleasePackage>;
|
|
330
|
+
type ReleaseTagRecord = CoreReleaseTagRecord<ReleasePackage>;
|
|
152
331
|
|
|
153
332
|
async function getReleaseState(root: string): Promise<ReleaseState> {
|
|
154
|
-
|
|
333
|
+
return getReleaseStateForPackages(root, listReleasePackages(root));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function getReleaseStateForPackages(root: string, packages: ReleasePackage[]): Promise<ReleaseState> {
|
|
155
337
|
const states = await Promise.all(
|
|
156
338
|
packages.map(async (pkg) => {
|
|
157
|
-
const published = await npmVersionExists(pkg.name, pkg.version);
|
|
339
|
+
const published = await npmVersionExists(root, pkg.name, pkg.version);
|
|
158
340
|
return { name: pkg.name, version: pkg.version, published };
|
|
159
341
|
}),
|
|
160
342
|
);
|
|
161
343
|
return { packages: states, allPublished: states.every((state) => state.published) };
|
|
162
344
|
}
|
|
163
345
|
|
|
164
|
-
async function listUnpublishedPackages(root: string): Promise<
|
|
165
|
-
const packages = listPublicPackages(root);
|
|
346
|
+
async function listUnpublishedPackages(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
166
347
|
const states = await Promise.all(
|
|
167
|
-
packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(pkg.name, pkg.version) })),
|
|
348
|
+
packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(root, pkg.name, pkg.version) })),
|
|
168
349
|
);
|
|
169
350
|
return states.filter((state) => !state.published).map((state) => state.pkg);
|
|
170
351
|
}
|
|
171
352
|
|
|
172
|
-
async function publishPackedPackage(root: string, pkg:
|
|
353
|
+
async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
|
|
173
354
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
174
355
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
175
356
|
try {
|
|
176
357
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
177
358
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
178
|
-
await
|
|
179
|
-
// npm CLI owns authentication here: trusted publishing OIDC when configured
|
|
180
|
-
// or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
|
|
359
|
+
await assertPackedWorkspaceDependencies(root, tarball, pkg);
|
|
360
|
+
// npm CLI owns authentication here: trusted publishing OIDC when configured.
|
|
181
361
|
// Bun still produces the tarball so workspace:* dependencies are resolved the
|
|
182
362
|
// same way smoo validates packed packages before release.
|
|
183
363
|
const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
|
|
184
364
|
if (dryRun) {
|
|
185
365
|
args.push('--dry-run');
|
|
186
366
|
}
|
|
187
|
-
await
|
|
367
|
+
await publishWithAuthDiagnostics(
|
|
368
|
+
pkg,
|
|
369
|
+
{
|
|
370
|
+
publish: () => runLatestNpmPublish(root, args),
|
|
371
|
+
versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
|
|
372
|
+
log: (message) => console.log(message),
|
|
373
|
+
error: (message) => console.error(message),
|
|
374
|
+
appendSummary: async (markdown) => {
|
|
375
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
376
|
+
if (summaryPath) {
|
|
377
|
+
await appendFile(summaryPath, `${markdown}\n\n`);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
tokenPresent: npmAuthTokenPresent(),
|
|
383
|
+
repository: githubRepositoryFromRootPackage(root),
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
} finally {
|
|
387
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function npmAuthTokenPresent(): boolean {
|
|
392
|
+
return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function publishPlaceholderPackage(
|
|
396
|
+
root: string,
|
|
397
|
+
pkg: ReleasePackage,
|
|
398
|
+
env?: Record<string, string>,
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-bootstrap-'));
|
|
401
|
+
try {
|
|
402
|
+
await writeFile(
|
|
403
|
+
join(tempDir, 'package.json'),
|
|
404
|
+
`${JSON.stringify(
|
|
405
|
+
{
|
|
406
|
+
name: pkg.name,
|
|
407
|
+
version: NPM_BOOTSTRAP_VERSION,
|
|
408
|
+
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
409
|
+
license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
|
|
410
|
+
repository: pkg.json.repository,
|
|
411
|
+
publishConfig: { access: 'public' },
|
|
412
|
+
},
|
|
413
|
+
null,
|
|
414
|
+
2,
|
|
415
|
+
)}\n`,
|
|
416
|
+
);
|
|
417
|
+
await writeFile(
|
|
418
|
+
join(tempDir, 'README.md'),
|
|
419
|
+
`# ${pkg.name}\n\nThis is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.\n`,
|
|
420
|
+
);
|
|
421
|
+
await runLatestNpm(root, ['publish', tempDir, '--access', 'public', '--tag', NPM_BOOTSTRAP_DIST_TAG], env);
|
|
188
422
|
} finally {
|
|
189
423
|
await rm(tempDir, { recursive: true, force: true });
|
|
190
424
|
}
|
|
@@ -194,29 +428,135 @@ function safeTarballPrefix(name: string): string {
|
|
|
194
428
|
return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
195
429
|
}
|
|
196
430
|
|
|
197
|
-
async function
|
|
198
|
-
const
|
|
431
|
+
async function assertPackedWorkspaceDependencies(root: string, tarball: string, pkg: ReleasePackage): Promise<void> {
|
|
432
|
+
const manifest = await readPackedPackageJson(root, tarball, pkg.name);
|
|
433
|
+
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
|
|
434
|
+
if (failures.length > 0) {
|
|
435
|
+
throw new Error(failures.join('\n'));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function releasePackages(root: string): ReleasePackage[] {
|
|
440
|
+
const packages = listReleasePackages(root);
|
|
441
|
+
if (packages.length === 0) {
|
|
442
|
+
throw new Error('No owned release packages found.');
|
|
443
|
+
}
|
|
444
|
+
return packages;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function releasePackageProjects(packages: ReleasePackage[]): string {
|
|
448
|
+
return packages.map((pkg) => pkg.projectName).join(',');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function releaseVersionPackages(
|
|
452
|
+
root: string,
|
|
453
|
+
packages: ReleasePackage[],
|
|
454
|
+
bump: string,
|
|
455
|
+
): Promise<ReleasePackage[]> {
|
|
456
|
+
if (bump !== 'auto') {
|
|
457
|
+
return packages;
|
|
458
|
+
}
|
|
459
|
+
return autoReleaseCandidatePackages(
|
|
460
|
+
{
|
|
461
|
+
gitRefExists: (ref) => gitRefExists(root, ref),
|
|
462
|
+
packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
|
|
463
|
+
packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
|
|
464
|
+
currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
|
|
465
|
+
packageBuildInputPatterns: (projectName, packagePath) =>
|
|
466
|
+
packageBuildInputPatterns(root, projectName, packagePath),
|
|
467
|
+
packageHasHistory: (packagePath) => packageHasHistory(root, packagePath),
|
|
468
|
+
},
|
|
469
|
+
packages,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function runNxReleaseVersion(root: string, projects: string, bump: string, dryRun: boolean): Promise<void> {
|
|
474
|
+
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
475
|
+
// release commit, and annotated tags. smoo owns remote publication after the
|
|
476
|
+
// workflow validates the exact release commit Nx produced.
|
|
477
|
+
const nxArgs = ['release', 'version'];
|
|
478
|
+
if (bump !== 'auto') {
|
|
479
|
+
nxArgs.push(bump);
|
|
480
|
+
}
|
|
481
|
+
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
482
|
+
if (dryRun) {
|
|
483
|
+
nxArgs.push('--dry-run');
|
|
484
|
+
}
|
|
485
|
+
await run('nx', nxArgs, root);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function releasePackagesAtHead(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
489
|
+
return releasePackagesAtRef(root, packages, 'HEAD');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function writeReleaseGithubOutput(
|
|
493
|
+
outputPath: string | undefined,
|
|
494
|
+
packages: ReleasePackage[],
|
|
495
|
+
mode: ReleaseVersionMode,
|
|
496
|
+
): Promise<void> {
|
|
497
|
+
if (!outputPath) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
await appendFile(outputPath, `mode=${mode}\nprojects=${releasePackageProjects(packages)}\n`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function releasePackagesAtRef(root: string, packages: ReleasePackage[], ref: string): Promise<ReleasePackage[]> {
|
|
504
|
+
const packagesAtRef = await Promise.all(packages.map(async (pkg) => releasePackageAtRef(root, pkg, ref)));
|
|
505
|
+
const presentPackages = packagesAtRef.filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
506
|
+
const tags = new Set(await gitTagsAtRef(root, ref));
|
|
507
|
+
const taggedPackages = presentPackages.filter((pkg) => tags.has(releaseTag(pkg)));
|
|
508
|
+
if (taggedPackages.length > 0) {
|
|
509
|
+
return taggedPackages;
|
|
510
|
+
}
|
|
511
|
+
if (!(await isNxReleaseCommit(root, ref))) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
return releasePackagesChangedAtRef(root, presentPackages, ref);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function releasePackageAtRef(root: string, pkg: ReleasePackage, ref: string): Promise<ReleasePackage | null> {
|
|
518
|
+
const version = await packageVersionAtRef(root, pkg.path, ref);
|
|
519
|
+
return version ? { ...pkg, version } : null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function packageVersionAtRef(root: string, packagePath: string, ref: string): Promise<string | null> {
|
|
523
|
+
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
199
524
|
if (result.exitCode !== 0) {
|
|
200
|
-
|
|
525
|
+
return null;
|
|
201
526
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
527
|
+
try {
|
|
528
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
529
|
+
return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
205
532
|
}
|
|
206
533
|
}
|
|
207
534
|
|
|
208
|
-
async function
|
|
209
|
-
const result = await $`
|
|
210
|
-
return result.exitCode === 0 && decode(result.stdout).trim() ===
|
|
535
|
+
async function isNxReleaseCommit(root: string, ref: string): Promise<boolean> {
|
|
536
|
+
const result = await $`git show -s --format=%s ${ref}`.cwd(root).quiet().nothrow();
|
|
537
|
+
return result.exitCode === 0 && decode(result.stdout).trim() === 'chore(release): publish';
|
|
211
538
|
}
|
|
212
539
|
|
|
213
|
-
async function
|
|
214
|
-
|
|
215
|
-
|
|
540
|
+
async function releasePackagesChangedAtRef(
|
|
541
|
+
root: string,
|
|
542
|
+
packages: ReleasePackage[],
|
|
543
|
+
ref: string,
|
|
544
|
+
): Promise<ReleasePackage[]> {
|
|
545
|
+
if (!(await gitRefExists(root, `${ref}^`))) {
|
|
546
|
+
return packages;
|
|
547
|
+
}
|
|
548
|
+
const changed: ReleasePackage[] = [];
|
|
549
|
+
for (const pkg of packages) {
|
|
550
|
+
const previousVersion = await packageVersionAtRef(root, pkg.path, `${ref}^`);
|
|
551
|
+
if (previousVersion !== pkg.version) {
|
|
552
|
+
changed.push(pkg);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return changed;
|
|
216
556
|
}
|
|
217
557
|
|
|
218
|
-
async function
|
|
219
|
-
const result = await $`git tag --points-at
|
|
558
|
+
async function gitTagsAtRef(root: string, ref: string): Promise<string[]> {
|
|
559
|
+
const result = await $`git tag --points-at ${ref}`.cwd(root).quiet().nothrow();
|
|
220
560
|
if (result.exitCode !== 0) {
|
|
221
561
|
return [];
|
|
222
562
|
}
|
|
@@ -226,6 +566,512 @@ async function gitTagsAtHead(root: string): Promise<string[]> {
|
|
|
226
566
|
.filter(Boolean);
|
|
227
567
|
}
|
|
228
568
|
|
|
569
|
+
async function previousReleaseTag(root: string, pkg: ReleasePackage, currentTag: string): Promise<string | null> {
|
|
570
|
+
const result = await $`git tag --list ${`${pkg.name}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
571
|
+
.cwd(root)
|
|
572
|
+
.quiet()
|
|
573
|
+
.nothrow();
|
|
574
|
+
if (result.exitCode !== 0) {
|
|
575
|
+
throw new Error(`Unable to list release tags for ${pkg.name}.`);
|
|
576
|
+
}
|
|
577
|
+
return (
|
|
578
|
+
decode(result.stdout)
|
|
579
|
+
.split('\n')
|
|
580
|
+
.map((tag) => tag.trim())
|
|
581
|
+
.find((tag) => tag && tag !== currentTag) ?? null
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function ensureLocalReleaseTags(root: string, packages: ReleasePackage[]): Promise<void> {
|
|
586
|
+
for (const pkg of packages) {
|
|
587
|
+
const tag = releaseTag(pkg);
|
|
588
|
+
if (await gitRefExists(root, `refs/tags/${tag}`)) {
|
|
589
|
+
if (!(await gitTagPointsAt(root, tag, 'HEAD'))) {
|
|
590
|
+
throw new Error(`Release tag ${tag} exists but does not point at HEAD.`);
|
|
591
|
+
}
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
await run('git', ['tag', '-a', tag, '-m', tag, 'HEAD'], root);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function pushReleaseRefs(root: string, packages: ReleasePackage[]): Promise<boolean> {
|
|
599
|
+
const branch = await releaseBranch(root);
|
|
600
|
+
const remote = await releaseRemote(root, branch);
|
|
601
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
602
|
+
await ensureLocalReleaseTags(root, packages);
|
|
603
|
+
const refspecs: string[] = [];
|
|
604
|
+
const remoteRef = `${remote}/${branch}`;
|
|
605
|
+
const head = await gitHead(root);
|
|
606
|
+
if (!(await gitRefExists(root, remoteRef)) || !(await gitIsAncestor(root, head, remoteRef))) {
|
|
607
|
+
refspecs.push(`HEAD:refs/heads/${branch}`);
|
|
608
|
+
}
|
|
609
|
+
for (const pkg of packages) {
|
|
610
|
+
const tag = releaseTag(pkg);
|
|
611
|
+
if (!(await remoteReleaseTagExists(root, remote, tag))) {
|
|
612
|
+
refspecs.push(`refs/tags/${tag}:refs/tags/${tag}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (refspecs.length === 0) {
|
|
616
|
+
console.log('Release branch and tags are already present on the remote.');
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
await run('git', ['push', '--atomic', remote, ...refspecs], root);
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePackage> {
|
|
624
|
+
return {
|
|
625
|
+
gitHead: () => gitHead(root),
|
|
626
|
+
pushReleaseRefs: (packages) => pushReleaseRefs(root, packages),
|
|
627
|
+
listNpmMissingPackages: (packages) => listUnpublishedPackages(root, packages),
|
|
628
|
+
buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
|
|
629
|
+
publishPackage: async (pkg, distTag, dryRun) => {
|
|
630
|
+
const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
|
|
631
|
+
if (!packageExists) {
|
|
632
|
+
throw new Error(missingNpmPackagePublishGuidance(pkg));
|
|
633
|
+
}
|
|
634
|
+
await publishPackedPackage(root, pkg, distTag, dryRun);
|
|
635
|
+
},
|
|
636
|
+
listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
|
|
637
|
+
createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage> & {
|
|
642
|
+
checkout(ref: string): Promise<void>;
|
|
643
|
+
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
644
|
+
beforeRepairTarget(target: ReleaseTarget): void;
|
|
645
|
+
afterRepairTarget(target: ReleaseTarget): void;
|
|
646
|
+
} {
|
|
647
|
+
return {
|
|
648
|
+
...releaseCompletionShell(root),
|
|
649
|
+
checkout: (ref) => run('git', ['switch', '--detach', ref], root),
|
|
650
|
+
withDevenvEnv: (runWithEnv) => withDevenvEnv(root, runWithEnv),
|
|
651
|
+
beforeRepairTarget: (target) => {
|
|
652
|
+
console.log(`::group::Repair pending release ${target.sha.slice(0, 12)} (${packageSummary(target.packages)})`);
|
|
653
|
+
},
|
|
654
|
+
afterRepairTarget: () => {
|
|
655
|
+
console.log('::endgroup::');
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function releaseRetagShell(root: string, remote: string) {
|
|
661
|
+
return {
|
|
662
|
+
listReleasePackages: () => releasePackages(root),
|
|
663
|
+
resolveRef: (ref: string) => gitSha(root, ref),
|
|
664
|
+
resolveDispatchRef: (branch: string) => remoteBranchSha(root, remote, branch),
|
|
665
|
+
packageVersionAtRef: (packagePath: string, ref: string) => packageVersionAtRef(root, packagePath, ref),
|
|
666
|
+
npmVersionExists: (name: string, version: string) => npmVersionExists(root, name, version),
|
|
667
|
+
githubReleaseExists: (tag: string) => githubReleaseExists(root, tag),
|
|
668
|
+
remoteTagObject: (tag: string) => remoteReleaseTagObject(root, remote, tag),
|
|
669
|
+
createOrMoveTag: (tag: string, ref: string) => run('git', ['tag', '-fa', tag, '-m', tag, ref], root),
|
|
670
|
+
pushTags: (updates: RetagUnpublishedTagUpdate<ReleasePackage>[]) => pushRetaggedReleaseTags(root, remote, updates),
|
|
671
|
+
dispatchPublishWorkflow: (workflow: string, branch: string) =>
|
|
672
|
+
run('gh', ['workflow', 'run', workflow, '--ref', branch, '-f', 'bump=auto', '-f', 'dry_run=false'], root),
|
|
673
|
+
log: (message: string) => console.log(message),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function listPendingReleaseTargets(root: string, ref: string): Promise<ReleaseTarget[]> {
|
|
678
|
+
const head = await gitHead(root);
|
|
679
|
+
return pendingReleaseTargets(await listOwnedReleaseTagRecords(root, ref), head);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function listOwnedReleaseTagRecords(root: string, ref: string): Promise<ReleaseTagRecord[]> {
|
|
683
|
+
const tags = await gitReleaseTagsByCreatorDate(root);
|
|
684
|
+
console.log(`Repair pending releases: scanning ${tags.length} local tag${tags.length === 1 ? '' : 's'}.`);
|
|
685
|
+
return collectOwnedReleaseTagRecords(releasePackages(root), ref, {
|
|
686
|
+
listReleaseTagsByCreatorDate: async () => tags,
|
|
687
|
+
isAncestor: (ancestor, descendant) => gitIsAncestor(root, ancestor, descendant),
|
|
688
|
+
packageVersionAtRef: (packagePath, tagRef) => packageVersionAtRef(root, packagePath, tagRef),
|
|
689
|
+
durableTagState: (pkg, tag) => durableReleaseTagState(root, pkg, tag),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'name' | 'version'>, tag: string) {
|
|
694
|
+
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
695
|
+
const start = Date.now();
|
|
696
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${tag}).`);
|
|
697
|
+
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
698
|
+
npmVersionExists(root, pkg.name, pkg.version),
|
|
699
|
+
githubReleaseExists(root, tag),
|
|
700
|
+
]);
|
|
701
|
+
console.log(
|
|
702
|
+
`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`,
|
|
703
|
+
);
|
|
704
|
+
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function gitReleaseTagsByCreatorDate(root: string): Promise<GitReleaseTagInfo[]> {
|
|
708
|
+
const result =
|
|
709
|
+
await $`git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
710
|
+
.cwd(root)
|
|
711
|
+
.quiet()
|
|
712
|
+
.nothrow();
|
|
713
|
+
if (result.exitCode !== 0) {
|
|
714
|
+
throw new Error('Unable to list release tags by creator date.');
|
|
715
|
+
}
|
|
716
|
+
return decode(result.stdout)
|
|
717
|
+
.split('\n')
|
|
718
|
+
.map((line) => line.trim())
|
|
719
|
+
.filter(Boolean)
|
|
720
|
+
.map((line) => {
|
|
721
|
+
const [name, timestampText, peeledSha, objectSha] = line.split('\t');
|
|
722
|
+
const timestamp = Number(timestampText);
|
|
723
|
+
const sha = peeledSha || objectSha;
|
|
724
|
+
if (!name || !sha || !Number.isSafeInteger(timestamp)) {
|
|
725
|
+
throw new Error(`Unable to parse release tag ref: ${line}`);
|
|
726
|
+
}
|
|
727
|
+
return { name, sha, timestamp };
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function buildReleaseCandidate(root: string, packages: ReleasePackage[]): Promise<void> {
|
|
732
|
+
await run('nx', ['run-many', '-t', 'build', `--projects=${releasePackageProjects(packages)}`], root);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function listMissingGithubReleasePackages(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
736
|
+
return (
|
|
737
|
+
await Promise.all(packages.map(async (pkg) => ((await githubReleaseExists(root, releaseTag(pkg))) ? null : pkg)))
|
|
738
|
+
).filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<void> {
|
|
742
|
+
const currentTag = releaseTag(pkg);
|
|
743
|
+
console.log(`${pkg.name}@${pkg.version}: rendering GitHub Release notes for ${currentTag}.`);
|
|
744
|
+
console.log(`GitHub release auth: ${envPresence('GH_TOKEN')}, ${envPresence('GITHUB_TOKEN')}.`);
|
|
745
|
+
if (!dryRun) {
|
|
746
|
+
await assertRemoteTagExists(root, currentTag);
|
|
747
|
+
}
|
|
748
|
+
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
749
|
+
if (dryRun) {
|
|
750
|
+
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
754
|
+
await createOrUpdateGithubRelease(pkg, contents, {
|
|
755
|
+
githubReleaseExists: (tag) => githubReleaseExists(root, tag),
|
|
756
|
+
runGhRelease: (args) => run('gh', args, root),
|
|
757
|
+
log: (message) => console.log(message),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function envPresence(name: string): string {
|
|
762
|
+
return `${name}=${process.env[name] ? 'present' : 'missing'}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function yesNo(value: boolean): string {
|
|
766
|
+
return value ? 'yes' : 'no';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function newerCommitsRemain(root: string): Promise<boolean> {
|
|
770
|
+
const branch = await releaseBranch(root);
|
|
771
|
+
const remote = await releaseRemote(root, branch);
|
|
772
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
773
|
+
const remoteRef = `${remote}/${branch}`;
|
|
774
|
+
if (!(await gitRefExists(root, remoteRef))) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
const head = await gitHead(root);
|
|
778
|
+
return head !== (await gitSha(root, remoteRef)) && (await gitIsAncestor(root, head, remoteRef));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Promise<void> {
|
|
782
|
+
const shortSha = summary.sha.slice(0, 12);
|
|
783
|
+
const lines = [
|
|
784
|
+
'## Release Summary',
|
|
785
|
+
'',
|
|
786
|
+
`- Commit: \`${shortSha}\``,
|
|
787
|
+
`- Mode: ${summary.dryRun ? 'dry run' : 'release'}`,
|
|
788
|
+
];
|
|
789
|
+
if (summary.noRelease) {
|
|
790
|
+
lines.push('- Result: no release tags found at HEAD');
|
|
791
|
+
} else {
|
|
792
|
+
lines.push(`- Packages: ${packageSummary(summary.packages)}`);
|
|
793
|
+
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
794
|
+
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
795
|
+
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
796
|
+
lines.push(`- GitHub Releases created/updated: ${packageSummary(summary.githubReleases)}`);
|
|
797
|
+
}
|
|
798
|
+
if (summary.rerunRequired) {
|
|
799
|
+
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
800
|
+
console.log(`::warning::${message}`);
|
|
801
|
+
lines.push(`- Warning: ${message}`);
|
|
802
|
+
}
|
|
803
|
+
const text = `${lines.join('\n')}\n`;
|
|
804
|
+
console.log(text.trimEnd());
|
|
805
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
806
|
+
if (summaryPath) {
|
|
807
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function writeRepairSummary(summaries: Array<ReleaseSummary<ReleasePackage>>, dryRun: boolean): Promise<void> {
|
|
812
|
+
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
813
|
+
if (summaries.length === 0) {
|
|
814
|
+
lines.push('- Result: no pending releases needed repair');
|
|
815
|
+
} else {
|
|
816
|
+
for (const summary of summaries) {
|
|
817
|
+
lines.push(`- Repaired \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const text = `${lines.join('\n')}\n`;
|
|
821
|
+
console.log(text.trimEnd());
|
|
822
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
823
|
+
if (summaryPath) {
|
|
824
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function packageSummary(packages: ReleasePackage[]): string {
|
|
829
|
+
if (packages.length === 0) {
|
|
830
|
+
return 'none';
|
|
831
|
+
}
|
|
832
|
+
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
|
|
836
|
+
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function gitRefExists(root: string, ref: string): Promise<boolean> {
|
|
840
|
+
const result = await $`git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
|
|
841
|
+
return result.exitCode === 0;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function packageChangedFilesSince(root: string, ref: string, packagePath: string): Promise<string[]> {
|
|
845
|
+
const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
846
|
+
if (result.exitCode !== 0) {
|
|
847
|
+
throw new Error(`Unable to inspect package changes under ${packagePath}.`);
|
|
848
|
+
}
|
|
849
|
+
const packagePrefix = `${packagePath}/`;
|
|
850
|
+
return decode(result.stdout)
|
|
851
|
+
.split('\n')
|
|
852
|
+
.map((path) => path.trim())
|
|
853
|
+
.filter(Boolean)
|
|
854
|
+
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function packageJsonAtRef(
|
|
858
|
+
root: string,
|
|
859
|
+
ref: string,
|
|
860
|
+
packagePath: string,
|
|
861
|
+
): Promise<Record<string, unknown> | null> {
|
|
862
|
+
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
863
|
+
if (result.exitCode !== 0) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
868
|
+
return isRecord(parsed) ? parsed : null;
|
|
869
|
+
} catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function currentPackageJson(root: string, packagePath: string): Promise<Record<string, unknown> | null> {
|
|
875
|
+
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
|
|
879
|
+
const project = await nxProjectJson(root, projectName);
|
|
880
|
+
const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
|
|
881
|
+
return resolveBuildInputPatterns(project, nxJson);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function nxProjectJson(root: string, projectName: string): Promise<Record<string, unknown>> {
|
|
885
|
+
const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
|
|
886
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
887
|
+
if (!isRecord(parsed)) {
|
|
888
|
+
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
889
|
+
}
|
|
890
|
+
return parsed;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function resolveBuildInputPatterns(project: Record<string, unknown>, nxJson: Record<string, unknown>): string[] {
|
|
894
|
+
const targets = recordProperty(project, 'targets');
|
|
895
|
+
if (!targets) {
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function collectBuildInputs(targets: Record<string, unknown>): string[] {
|
|
902
|
+
const build = recordProperty(targets, 'build');
|
|
903
|
+
if (!build) {
|
|
904
|
+
return ['production'];
|
|
905
|
+
}
|
|
906
|
+
const directInputs = stringArrayProperty(build, 'inputs');
|
|
907
|
+
if (directInputs.length > 0) {
|
|
908
|
+
return directInputs;
|
|
909
|
+
}
|
|
910
|
+
const inputs: string[] = [];
|
|
911
|
+
for (const dependency of stringArrayProperty(build, 'dependsOn')) {
|
|
912
|
+
if (dependency.startsWith('^')) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
916
|
+
if (!targetName) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
|
|
920
|
+
}
|
|
921
|
+
return inputs.length > 0 ? inputs : ['production'];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown>): string[] {
|
|
925
|
+
const patterns: string[] = [];
|
|
926
|
+
const seen = new Set<string>();
|
|
927
|
+
for (const input of inputs) {
|
|
928
|
+
for (const pattern of expandInputPattern(input, nxJson, seen)) {
|
|
929
|
+
patterns.push(pattern);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return patterns;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen: Set<string>): string[] {
|
|
936
|
+
if (seen.has(input)) {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
seen.add(input);
|
|
940
|
+
if (!input.includes('{')) {
|
|
941
|
+
const namedInputs = recordProperty(nxJson, 'namedInputs');
|
|
942
|
+
const namedInput = namedInputs?.[input];
|
|
943
|
+
if (Array.isArray(namedInput)) {
|
|
944
|
+
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
945
|
+
}
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
948
|
+
const excluded = input.startsWith('!');
|
|
949
|
+
const rawInput = excluded ? input.slice(1) : input;
|
|
950
|
+
if (!rawInput.startsWith('{projectRoot}/')) {
|
|
951
|
+
return [];
|
|
952
|
+
}
|
|
953
|
+
const packageRelative = rawInput.slice('{projectRoot}/'.length);
|
|
954
|
+
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
958
|
+
if (!record) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const value = record[key];
|
|
962
|
+
return isRecord(value) ? value : null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
|
|
966
|
+
const value = record?.[key];
|
|
967
|
+
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
|
|
971
|
+
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
972
|
+
if (result.exitCode !== 0) {
|
|
973
|
+
throw new Error(`Unable to inspect package history under ${packagePath}.`);
|
|
974
|
+
}
|
|
975
|
+
return decode(result.stdout).trim().length > 0;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function gitIsAncestor(root: string, ancestor: string, descendant: string): Promise<boolean> {
|
|
979
|
+
const result = await $`git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
|
|
980
|
+
return result.exitCode === 0;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function gitSha(root: string, ref: string): Promise<string> {
|
|
984
|
+
const result = await $`git rev-parse ${ref}`.cwd(root).quiet().nothrow();
|
|
985
|
+
if (result.exitCode !== 0) {
|
|
986
|
+
throw new Error(`Unable to resolve git ref ${ref}.`);
|
|
987
|
+
}
|
|
988
|
+
return decode(result.stdout).trim();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function gitTagPointsAt(root: string, tag: string, ref: string): Promise<boolean> {
|
|
992
|
+
return (await gitCommitForTag(root, tag)) === (await gitSha(root, ref));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function gitCommitForTag(root: string, tag: string): Promise<string> {
|
|
996
|
+
const result = await $`git rev-list -n 1 ${tag}`.cwd(root).quiet().nothrow();
|
|
997
|
+
if (result.exitCode !== 0) {
|
|
998
|
+
throw new Error(`Unable to resolve release tag ${tag}.`);
|
|
999
|
+
}
|
|
1000
|
+
return decode(result.stdout).trim();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function remoteReleaseTagExists(root: string, remote: string, tag: string): Promise<boolean> {
|
|
1004
|
+
const result = await $`git ls-remote --exit-code --tags ${remote} ${`refs/tags/${tag}`}`.cwd(root).quiet().nothrow();
|
|
1005
|
+
return result.exitCode === 0;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function remoteReleaseTagObject(root: string, remote: string, tag: string): Promise<string | null> {
|
|
1009
|
+
const result = await $`git ls-remote --exit-code --tags ${remote} ${`refs/tags/${tag}`}`.cwd(root).quiet().nothrow();
|
|
1010
|
+
if (result.exitCode === 2) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
if (result.exitCode !== 0) {
|
|
1014
|
+
throw new Error(`Unable to inspect remote release tag ${tag} on ${remote}.`);
|
|
1015
|
+
}
|
|
1016
|
+
const [object] = decode(result.stdout).trim().split('\t');
|
|
1017
|
+
if (!object) {
|
|
1018
|
+
throw new Error(`Unable to parse remote release tag ${tag} on ${remote}.`);
|
|
1019
|
+
}
|
|
1020
|
+
return object;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function remoteBranchSha(root: string, remote: string, branch: string): Promise<string | null> {
|
|
1024
|
+
const result = await $`git ls-remote --exit-code --heads ${remote} ${`refs/heads/${branch}`}`
|
|
1025
|
+
.cwd(root)
|
|
1026
|
+
.quiet()
|
|
1027
|
+
.nothrow();
|
|
1028
|
+
if (result.exitCode === 2) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
if (result.exitCode !== 0) {
|
|
1032
|
+
throw new Error(`Unable to inspect remote branch ${branch} on ${remote}.`);
|
|
1033
|
+
}
|
|
1034
|
+
const [sha] = decode(result.stdout).trim().split('\t');
|
|
1035
|
+
if (!sha) {
|
|
1036
|
+
throw new Error(`Unable to parse remote branch ${branch} on ${remote}.`);
|
|
1037
|
+
}
|
|
1038
|
+
return sha;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async function pushRetaggedReleaseTags(
|
|
1042
|
+
root: string,
|
|
1043
|
+
remote: string,
|
|
1044
|
+
updates: Array<RetagUnpublishedTagUpdate<ReleasePackage>>,
|
|
1045
|
+
): Promise<void> {
|
|
1046
|
+
const leases = updates.map(
|
|
1047
|
+
(update) => `--force-with-lease=refs/tags/${update.tag}:${update.expectedRemoteObject ?? ''}`,
|
|
1048
|
+
);
|
|
1049
|
+
const refspecs = updates.map((update) => `refs/tags/${update.tag}:refs/tags/${update.tag}`);
|
|
1050
|
+
await run('git', ['push', '--atomic', ...leases, remote, ...refspecs], root);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function githubReleaseExists(root: string, tag: string): Promise<boolean> {
|
|
1054
|
+
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function assertRemoteTagExists(root: string, tag: string): Promise<void> {
|
|
1058
|
+
const branch = await releaseBranch(root);
|
|
1059
|
+
const remote = await releaseRemote(root, branch);
|
|
1060
|
+
if (!(await remoteReleaseTagExists(root, remote, tag))) {
|
|
1061
|
+
throw new Error(`Release tag ${tag} is not present on remote ${remote}. Run smoo release version first.`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function npmVersionExists(root: string, name: string, version: string): Promise<boolean> {
|
|
1066
|
+
const result = await $`bun pm view ${`${name}@${version}`} version`.cwd(root).quiet().nothrow();
|
|
1067
|
+
return result.exitCode === 0 && decode(result.stdout).trim() === version;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function npmPackageExists(root: string, name: string): Promise<boolean> {
|
|
1071
|
+
const result = await $`bun pm view ${name} name`.cwd(root).quiet().nothrow();
|
|
1072
|
+
return result.exitCode === 0;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
229
1075
|
async function assertCleanGitTree(root: string): Promise<void> {
|
|
230
1076
|
const result = await $`git status --porcelain --untracked-files=no`.cwd(root).quiet().nothrow();
|
|
231
1077
|
if (result.exitCode !== 0) {
|
|
@@ -245,12 +1091,6 @@ async function gitHead(root: string): Promise<string> {
|
|
|
245
1091
|
return decode(result.stdout).trim();
|
|
246
1092
|
}
|
|
247
1093
|
|
|
248
|
-
async function pushReleaseCommit(root: string): Promise<void> {
|
|
249
|
-
const branch = await releaseBranch(root);
|
|
250
|
-
const remote = await releaseRemote(root, branch);
|
|
251
|
-
await run('git', ['push', '--follow-tags', '--atomic', remote, `HEAD:refs/heads/${branch}`], root);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
1094
|
async function releaseBranch(root: string): Promise<string> {
|
|
255
1095
|
const githubBranch = process.env.GITHUB_REF_NAME?.trim();
|
|
256
1096
|
if (githubBranch) {
|
|
@@ -304,19 +1144,6 @@ function releaseBumpArg(bump = 'auto'): string {
|
|
|
304
1144
|
return bump;
|
|
305
1145
|
}
|
|
306
1146
|
|
|
307
|
-
function releaseNpmTagArg(options: ReleasePublishOptions): string {
|
|
308
|
-
const bump = releaseBumpArg(options.bump);
|
|
309
|
-
const derivedTag = bump === 'prerelease' ? 'next' : 'latest';
|
|
310
|
-
const explicitTag = options.tag ?? options.npmTag;
|
|
311
|
-
if (!explicitTag) {
|
|
312
|
-
return derivedTag;
|
|
313
|
-
}
|
|
314
|
-
if (explicitTag !== derivedTag) {
|
|
315
|
-
throw new Error(`--bump ${bump} publishes with npm dist-tag ${derivedTag}, not ${explicitTag}.`);
|
|
316
|
-
}
|
|
317
|
-
return explicitTag;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
1147
|
function githubRepositoryFromRootPackage(root: string): string {
|
|
321
1148
|
const pkg = readPackageJson(join(root, 'package.json'));
|
|
322
1149
|
const repository = pkg ? repositoryInfo(pkg.json) : null;
|
|
@@ -350,21 +1177,30 @@ async function runLatestNpm(root: string, npmArgs: string[], env?: Record<string
|
|
|
350
1177
|
await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
351
1178
|
}
|
|
352
1179
|
|
|
353
|
-
async function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1180
|
+
async function runLatestNpmTrust(
|
|
1181
|
+
root: string,
|
|
1182
|
+
npmArgs: string[],
|
|
1183
|
+
env?: Record<string, string>,
|
|
1184
|
+
): Promise<TrustPublisherResult> {
|
|
1185
|
+
const result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
1186
|
+
if (result.exitCode === 0) {
|
|
1187
|
+
return 'configured';
|
|
358
1188
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
await writeFile(npmrc, `registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`);
|
|
364
|
-
await runLatestNpm(root, npmArgs, { NPM_CONFIG_USERCONFIG: npmrc });
|
|
365
|
-
} finally {
|
|
366
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
1189
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1190
|
+
if (/\bE409\b|\b409 Conflict\b/.test(output)) {
|
|
1191
|
+
return 'already-configured';
|
|
367
1192
|
}
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${result.exitCode}`,
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
1199
|
+
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function missingNpmPackagePublishGuidance(pkg: Pick<ReleasePackage, 'name'>): string {
|
|
1203
|
+
return `${pkg.name} does not exist on npm yet. Run smoo release trust-publisher --bootstrap locally before rerunning the Publish workflow.`;
|
|
368
1204
|
}
|
|
369
1205
|
|
|
370
1206
|
async function promptForNpmOtp(packageName: string): Promise<string> {
|