@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
package/dist/release/index.js
CHANGED
|
@@ -1,89 +1,87 @@
|
|
|
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 { withDevenvEnv } from '../lib/devenv.js';
|
|
8
|
+
import { isRecord, stringProperty } from '../lib/json.js';
|
|
7
9
|
import { decode, run, runStatus } from '../lib/run.js';
|
|
8
|
-
import {
|
|
10
|
+
import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
|
|
11
|
+
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
12
|
+
import { collectOwnedReleaseTagRecords, pendingReleaseTargets, releaseTag, } from './core.js';
|
|
13
|
+
import { createOrUpdateGithubRelease, renderNxProjectChangelogContents } from './github-release.js';
|
|
14
|
+
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
15
|
+
import { completeReleaseAtHead as completeReleaseAtHeadWithShell, repairPendingTargets, runReleaseVersion, } from './orchestration.js';
|
|
16
|
+
import { retagUnpublished } from './retag-unpublished.js';
|
|
9
17
|
export async function releaseVersion(root, options) {
|
|
10
18
|
const bump = releaseBumpArg(options.bump);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
28
|
-
if (options.dryRun) {
|
|
29
|
-
nxArgs.push('--dryRun');
|
|
30
|
-
}
|
|
31
|
-
await run('nx', nxArgs, root);
|
|
32
|
-
if (!options.dryRun) {
|
|
33
|
-
// Guard against future Nx/config regressions that leave release files, such
|
|
34
|
-
// as bun.lock, outside the release commit after tagging/pushing.
|
|
35
|
-
await assertCleanGitTree(root);
|
|
36
|
-
const headAfterVersioning = await gitHead(root);
|
|
37
|
-
if (headAfterVersioning === headBeforeVersioning) {
|
|
38
|
-
console.log('Nx did not create a release commit; skipping git push.');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
await pushReleaseCommit(root);
|
|
42
|
-
}
|
|
19
|
+
const packages = releasePackages(root);
|
|
20
|
+
const projects = releasePackageProjects(packages);
|
|
21
|
+
const result = await runReleaseVersion({
|
|
22
|
+
releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
|
|
23
|
+
ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
|
|
24
|
+
gitHead: () => gitHead(root),
|
|
25
|
+
runNxReleaseVersion: (releaseBump, dryRun) => runNxReleaseVersion(root, projects, releaseBump, dryRun),
|
|
26
|
+
assertCleanGitTree: () => assertCleanGitTree(root),
|
|
27
|
+
}, { bump, dryRun: options.dryRun === true });
|
|
28
|
+
if (result.status === 'already-release-target') {
|
|
29
|
+
console.log('HEAD is already a release target; publish will complete any missing durable state.');
|
|
30
|
+
}
|
|
31
|
+
else if (result.status === 'no-release-needed') {
|
|
32
|
+
console.log('Nx did not create a release commit; no release needed.');
|
|
33
|
+
}
|
|
34
|
+
await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
|
|
43
35
|
}
|
|
44
36
|
export async function releasePublish(root, options) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const tags = options.tags ? options.tags.split(/\s+/).filter(Boolean) : await gitTagsAtHead(root);
|
|
64
|
-
if (tags.length === 0) {
|
|
65
|
-
throw new Error('No release tags found. Pass --tags or run from a tagged release commit.');
|
|
66
|
-
}
|
|
67
|
-
const latestFlag = npmTag === 'latest' ? 'true' : 'false';
|
|
68
|
-
for (const tag of tags) {
|
|
69
|
-
const exists = (await runStatus('gh', ['release', 'view', tag], root, true)) === 0;
|
|
70
|
-
if (exists) {
|
|
71
|
-
await run('gh', ['release', 'edit', tag, '--title', tag, `--latest=${latestFlag}`], root);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
await run('gh', ['release', 'create', tag, '--title', tag, '--generate-notes', `--latest=${latestFlag}`], root);
|
|
37
|
+
const bump = releaseBumpArg(options.bump);
|
|
38
|
+
const packages = await releasePackagesAtHead(root, releasePackages(root));
|
|
39
|
+
if (packages.length === 0) {
|
|
40
|
+
if (bump === 'auto') {
|
|
41
|
+
console.log('No release tags found at HEAD; no release to publish.');
|
|
42
|
+
const summary = {
|
|
43
|
+
sha: await gitHead(root),
|
|
44
|
+
dryRun: options.dryRun === true,
|
|
45
|
+
packages,
|
|
46
|
+
pushed: false,
|
|
47
|
+
published: [],
|
|
48
|
+
alreadyPublished: [],
|
|
49
|
+
githubReleases: [],
|
|
50
|
+
rerunRequired: false,
|
|
51
|
+
noRelease: true,
|
|
52
|
+
};
|
|
53
|
+
await writeReleaseSummary(summary);
|
|
54
|
+
return;
|
|
75
55
|
}
|
|
56
|
+
throw new Error('No release tags found at HEAD for current package versions. Run smoo release version first.');
|
|
76
57
|
}
|
|
58
|
+
const summary = await completeReleaseAtHeadWithShell(releaseCompletionShell(root), packages, options.dryRun === true, await newerCommitsRemain(root));
|
|
59
|
+
await writeReleaseSummary(summary);
|
|
60
|
+
}
|
|
61
|
+
export async function releaseRepairPending(root, options) {
|
|
62
|
+
const branch = await releaseBranch(root);
|
|
63
|
+
const remote = await releaseRemote(root, branch);
|
|
64
|
+
console.log(`Repair pending releases: fetching ${remote}/${branch} and tags.`);
|
|
65
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
66
|
+
const remoteRef = `${remote}/${branch}`;
|
|
67
|
+
const restoreRef = (await gitRefExists(root, remoteRef)) ? remoteRef : await gitHead(root);
|
|
68
|
+
console.log(`Repair pending releases: planning from ${restoreRef}.`);
|
|
69
|
+
const targets = await listPendingReleaseTargets(root, restoreRef);
|
|
70
|
+
console.log(targets.length === 0
|
|
71
|
+
? 'Repair pending releases: no pending durable state repairs found.'
|
|
72
|
+
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`);
|
|
73
|
+
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
74
|
+
await writeRepairSummary(summaries, options.dryRun === true);
|
|
77
75
|
}
|
|
78
76
|
export async function releaseTrustPublisher(root, options) {
|
|
79
77
|
const repository = githubRepositoryFromRootPackage(root);
|
|
80
78
|
const workflow = 'publish.yml';
|
|
81
|
-
const packages =
|
|
79
|
+
const packages = listReleasePackages(root);
|
|
82
80
|
if (packages.length === 0) {
|
|
83
|
-
throw new Error('No
|
|
81
|
+
throw new Error('No owned release packages found.');
|
|
84
82
|
}
|
|
85
83
|
if (!options.dryRun) {
|
|
86
|
-
const packageStates = await Promise.all(packages.map(async (pkg) => ((await npmPackageExists(pkg.name)) ? null : pkg.name)));
|
|
84
|
+
const packageStates = await Promise.all(packages.map(async (pkg) => ((await npmPackageExists(root, pkg.name)) ? null : pkg.name)));
|
|
87
85
|
const missingPackages = packageStates.filter((name) => name !== null);
|
|
88
86
|
if (missingPackages.length > 0) {
|
|
89
87
|
throw new Error('npm trusted publishing can only be configured after packages exist on the registry. ' +
|
|
@@ -94,39 +92,80 @@ export async function releaseTrustPublisher(root, options) {
|
|
|
94
92
|
if (!options.dryRun && !options.skipLogin) {
|
|
95
93
|
await runLatestNpm(root, ['login', '--auth-type=web']);
|
|
96
94
|
}
|
|
95
|
+
const failedPackages = [];
|
|
97
96
|
for (const pkg of packages) {
|
|
98
97
|
console.log(`${pkg.name}: trusting GitHub Actions ${repository}/${workflow}`);
|
|
99
98
|
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
100
99
|
if (options.dryRun) {
|
|
101
100
|
args.push('--dry-run');
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
while (true) {
|
|
103
|
+
const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
|
|
104
|
+
try {
|
|
105
|
+
await runLatestNpm(root, args, otp ? { NPM_CONFIG_OTP: otp } : undefined);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
110
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
111
|
+
if (!options.dryRun) {
|
|
112
|
+
console.error('npm OTP codes are single-use. Generate a fresh OTP before retrying this package.');
|
|
113
|
+
}
|
|
114
|
+
if (options.otp || options.dryRun) {
|
|
115
|
+
failedPackages.push(pkg.name);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
console.error('Retrying this package. Press Ctrl-C to stop.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (failedPackages.length > 0) {
|
|
123
|
+
console.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
124
|
+
if (options.otp) {
|
|
125
|
+
console.error('Rerun smoo release trust-publisher without --otp to enter a fresh OTP for each failed package.');
|
|
126
|
+
}
|
|
105
127
|
}
|
|
106
128
|
}
|
|
129
|
+
export async function releaseRetagUnpublished(root, options) {
|
|
130
|
+
const toRef = options.to ?? 'HEAD';
|
|
131
|
+
const dispatch = options.dispatch === true;
|
|
132
|
+
const push = options.push === true || dispatch;
|
|
133
|
+
const branch = options.branch ?? (await releaseBranch(root));
|
|
134
|
+
const remote = options.remote ?? (push ? await releaseRemote(root, branch) : 'origin');
|
|
135
|
+
await retagUnpublished(releaseRetagShell(root, remote), {
|
|
136
|
+
tags: options.tags,
|
|
137
|
+
toRef,
|
|
138
|
+
push,
|
|
139
|
+
dispatch,
|
|
140
|
+
dryRun: options.dryRun === true,
|
|
141
|
+
branch,
|
|
142
|
+
workflow: 'publish.yml',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
107
145
|
export async function printReleaseState(root) {
|
|
108
146
|
console.log(JSON.stringify(await getReleaseState(root), null, 2));
|
|
109
147
|
}
|
|
110
148
|
async function getReleaseState(root) {
|
|
111
|
-
|
|
149
|
+
return getReleaseStateForPackages(root, listReleasePackages(root));
|
|
150
|
+
}
|
|
151
|
+
async function getReleaseStateForPackages(root, packages) {
|
|
112
152
|
const states = await Promise.all(packages.map(async (pkg) => {
|
|
113
|
-
const published = await npmVersionExists(pkg.name, pkg.version);
|
|
153
|
+
const published = await npmVersionExists(root, pkg.name, pkg.version);
|
|
114
154
|
return { name: pkg.name, version: pkg.version, published };
|
|
115
155
|
}));
|
|
116
156
|
return { packages: states, allPublished: states.every((state) => state.published) };
|
|
117
157
|
}
|
|
118
|
-
async function listUnpublishedPackages(root) {
|
|
119
|
-
const
|
|
120
|
-
const states = await Promise.all(packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(pkg.name, pkg.version) })));
|
|
158
|
+
async function listUnpublishedPackages(root, packages) {
|
|
159
|
+
const states = await Promise.all(packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(root, pkg.name, pkg.version) })));
|
|
121
160
|
return states.filter((state) => !state.published).map((state) => state.pkg);
|
|
122
161
|
}
|
|
123
|
-
async function publishPackedPackage(root, pkg, tag, dryRun) {
|
|
162
|
+
async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
|
|
124
163
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
125
164
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
126
165
|
try {
|
|
127
166
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
128
167
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
129
|
-
await
|
|
168
|
+
await assertPackedWorkspaceDependencies(root, tarball, pkg);
|
|
130
169
|
// npm CLI owns authentication here: trusted publishing OIDC when configured,
|
|
131
170
|
// or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
|
|
132
171
|
// Bun still produces the tarball so workspace:* dependencies are resolved the
|
|
@@ -135,35 +174,122 @@ async function publishPackedPackage(root, pkg, tag, dryRun) {
|
|
|
135
174
|
if (dryRun) {
|
|
136
175
|
args.push('--dry-run');
|
|
137
176
|
}
|
|
138
|
-
await
|
|
177
|
+
await publishWithAuthDiagnostics(pkg, {
|
|
178
|
+
publish: () => runLatestNpmPublish(root, args, useBootstrapToken),
|
|
179
|
+
versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
|
|
180
|
+
log: (message) => console.log(message),
|
|
181
|
+
error: (message) => console.error(message),
|
|
182
|
+
appendSummary: async (markdown) => {
|
|
183
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
184
|
+
if (summaryPath) {
|
|
185
|
+
await appendFile(summaryPath, `${markdown}\n\n`);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
}, {
|
|
189
|
+
useBootstrapToken,
|
|
190
|
+
tokenPresent: npmAuthTokenPresent(),
|
|
191
|
+
repository: githubRepositoryFromRootPackage(root),
|
|
192
|
+
});
|
|
139
193
|
}
|
|
140
194
|
finally {
|
|
141
195
|
await rm(tempDir, { recursive: true, force: true });
|
|
142
196
|
}
|
|
143
197
|
}
|
|
198
|
+
function npmAuthTokenPresent() {
|
|
199
|
+
return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
|
|
200
|
+
}
|
|
144
201
|
function safeTarballPrefix(name) {
|
|
145
202
|
return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
146
203
|
}
|
|
147
|
-
async function
|
|
148
|
-
const
|
|
204
|
+
async function assertPackedWorkspaceDependencies(root, tarball, pkg) {
|
|
205
|
+
const manifest = await readPackedPackageJson(root, tarball, pkg.name);
|
|
206
|
+
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
|
|
207
|
+
if (failures.length > 0) {
|
|
208
|
+
throw new Error(failures.join('\n'));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function releasePackages(root) {
|
|
212
|
+
const packages = listReleasePackages(root);
|
|
213
|
+
if (packages.length === 0) {
|
|
214
|
+
throw new Error('No owned release packages found.');
|
|
215
|
+
}
|
|
216
|
+
return packages;
|
|
217
|
+
}
|
|
218
|
+
function releasePackageProjects(packages) {
|
|
219
|
+
return packages.map((pkg) => pkg.name).join(',');
|
|
220
|
+
}
|
|
221
|
+
async function runNxReleaseVersion(root, projects, bump, dryRun) {
|
|
222
|
+
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
223
|
+
// release commit, and annotated tags. smoo owns remote publication after the
|
|
224
|
+
// workflow validates the exact release commit Nx produced.
|
|
225
|
+
const nxArgs = ['release', 'version'];
|
|
226
|
+
if (bump !== 'auto') {
|
|
227
|
+
nxArgs.push(bump);
|
|
228
|
+
}
|
|
229
|
+
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
230
|
+
if (dryRun) {
|
|
231
|
+
nxArgs.push('--dry-run');
|
|
232
|
+
}
|
|
233
|
+
await run('nx', nxArgs, root);
|
|
234
|
+
}
|
|
235
|
+
async function releasePackagesAtHead(root, packages) {
|
|
236
|
+
return releasePackagesAtRef(root, packages, 'HEAD');
|
|
237
|
+
}
|
|
238
|
+
async function writeReleaseGithubOutput(outputPath, packages, mode) {
|
|
239
|
+
if (!outputPath) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await appendFile(outputPath, `mode=${mode}\nprojects=${releasePackageProjects(packages)}\n`);
|
|
243
|
+
}
|
|
244
|
+
async function releasePackagesAtRef(root, packages, ref) {
|
|
245
|
+
const packagesAtRef = await Promise.all(packages.map(async (pkg) => releasePackageAtRef(root, pkg, ref)));
|
|
246
|
+
const presentPackages = packagesAtRef.filter((pkg) => pkg !== null);
|
|
247
|
+
const tags = new Set(await gitTagsAtRef(root, ref));
|
|
248
|
+
const taggedPackages = presentPackages.filter((pkg) => tags.has(releaseTag(pkg)));
|
|
249
|
+
if (taggedPackages.length > 0) {
|
|
250
|
+
return taggedPackages;
|
|
251
|
+
}
|
|
252
|
+
if (!(await isNxReleaseCommit(root, ref))) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return releasePackagesChangedAtRef(root, presentPackages, ref);
|
|
256
|
+
}
|
|
257
|
+
async function releasePackageAtRef(root, pkg, ref) {
|
|
258
|
+
const version = await packageVersionAtRef(root, pkg.path, ref);
|
|
259
|
+
return version ? { ...pkg, version } : null;
|
|
260
|
+
}
|
|
261
|
+
async function packageVersionAtRef(root, packagePath, ref) {
|
|
262
|
+
const result = await $ `git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
149
263
|
if (result.exitCode !== 0) {
|
|
150
|
-
|
|
264
|
+
return null;
|
|
151
265
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
268
|
+
return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
155
272
|
}
|
|
156
273
|
}
|
|
157
|
-
async function
|
|
158
|
-
const result = await $ `
|
|
159
|
-
return result.exitCode === 0 && decode(result.stdout).trim() ===
|
|
274
|
+
async function isNxReleaseCommit(root, ref) {
|
|
275
|
+
const result = await $ `git show -s --format=%s ${ref}`.cwd(root).quiet().nothrow();
|
|
276
|
+
return result.exitCode === 0 && decode(result.stdout).trim() === 'chore(release): publish';
|
|
160
277
|
}
|
|
161
|
-
async function
|
|
162
|
-
|
|
163
|
-
|
|
278
|
+
async function releasePackagesChangedAtRef(root, packages, ref) {
|
|
279
|
+
if (!(await gitRefExists(root, `${ref}^`))) {
|
|
280
|
+
return packages;
|
|
281
|
+
}
|
|
282
|
+
const changed = [];
|
|
283
|
+
for (const pkg of packages) {
|
|
284
|
+
const previousVersion = await packageVersionAtRef(root, pkg.path, `${ref}^`);
|
|
285
|
+
if (previousVersion !== pkg.version) {
|
|
286
|
+
changed.push(pkg);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return changed;
|
|
164
290
|
}
|
|
165
|
-
async function
|
|
166
|
-
const result = await $ `git tag --points-at
|
|
291
|
+
async function gitTagsAtRef(root, ref) {
|
|
292
|
+
const result = await $ `git tag --points-at ${ref}`.cwd(root).quiet().nothrow();
|
|
167
293
|
if (result.exitCode !== 0) {
|
|
168
294
|
return [];
|
|
169
295
|
}
|
|
@@ -172,6 +298,325 @@ async function gitTagsAtHead(root) {
|
|
|
172
298
|
.map((tag) => tag.trim())
|
|
173
299
|
.filter(Boolean);
|
|
174
300
|
}
|
|
301
|
+
async function previousReleaseTag(root, pkg, currentTag) {
|
|
302
|
+
const result = await $ `git tag --list ${`${pkg.name}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
303
|
+
.cwd(root)
|
|
304
|
+
.quiet()
|
|
305
|
+
.nothrow();
|
|
306
|
+
if (result.exitCode !== 0) {
|
|
307
|
+
throw new Error(`Unable to list release tags for ${pkg.name}.`);
|
|
308
|
+
}
|
|
309
|
+
return (decode(result.stdout)
|
|
310
|
+
.split('\n')
|
|
311
|
+
.map((tag) => tag.trim())
|
|
312
|
+
.find((tag) => tag && tag !== currentTag) ?? null);
|
|
313
|
+
}
|
|
314
|
+
async function ensureLocalReleaseTags(root, packages) {
|
|
315
|
+
for (const pkg of packages) {
|
|
316
|
+
const tag = releaseTag(pkg);
|
|
317
|
+
if (await gitRefExists(root, `refs/tags/${tag}`)) {
|
|
318
|
+
if (!(await gitTagPointsAt(root, tag, 'HEAD'))) {
|
|
319
|
+
throw new Error(`Release tag ${tag} exists but does not point at HEAD.`);
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
await run('git', ['tag', '-a', tag, '-m', tag, 'HEAD'], root);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function pushReleaseRefs(root, packages) {
|
|
327
|
+
const branch = await releaseBranch(root);
|
|
328
|
+
const remote = await releaseRemote(root, branch);
|
|
329
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
330
|
+
await ensureLocalReleaseTags(root, packages);
|
|
331
|
+
const refspecs = [];
|
|
332
|
+
const remoteRef = `${remote}/${branch}`;
|
|
333
|
+
const head = await gitHead(root);
|
|
334
|
+
if (!(await gitRefExists(root, remoteRef)) || !(await gitIsAncestor(root, head, remoteRef))) {
|
|
335
|
+
refspecs.push(`HEAD:refs/heads/${branch}`);
|
|
336
|
+
}
|
|
337
|
+
for (const pkg of packages) {
|
|
338
|
+
const tag = releaseTag(pkg);
|
|
339
|
+
if (!(await remoteReleaseTagExists(root, remote, tag))) {
|
|
340
|
+
refspecs.push(`refs/tags/${tag}:refs/tags/${tag}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (refspecs.length === 0) {
|
|
344
|
+
console.log('Release branch and tags are already present on the remote.');
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
await run('git', ['push', '--atomic', remote, ...refspecs], root);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
function releaseCompletionShell(root) {
|
|
351
|
+
return {
|
|
352
|
+
gitHead: () => gitHead(root),
|
|
353
|
+
pushReleaseRefs: (packages) => pushReleaseRefs(root, packages),
|
|
354
|
+
listNpmMissingPackages: (packages) => listUnpublishedPackages(root, packages),
|
|
355
|
+
buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
|
|
356
|
+
publishPackage: async (pkg, distTag, dryRun) => {
|
|
357
|
+
const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
|
|
358
|
+
await publishPackedPackage(root, pkg, distTag, dryRun, !dryRun && !packageExists);
|
|
359
|
+
},
|
|
360
|
+
listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
|
|
361
|
+
createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function releaseRepairShell(root) {
|
|
365
|
+
return {
|
|
366
|
+
...releaseCompletionShell(root),
|
|
367
|
+
checkout: (ref) => run('git', ['switch', '--detach', ref], root),
|
|
368
|
+
withDevenvEnv: (runWithEnv) => withDevenvEnv(root, runWithEnv),
|
|
369
|
+
beforeRepairTarget: (target) => {
|
|
370
|
+
console.log(`::group::Repair pending release ${target.sha.slice(0, 12)} (${packageSummary(target.packages)})`);
|
|
371
|
+
},
|
|
372
|
+
afterRepairTarget: () => {
|
|
373
|
+
console.log('::endgroup::');
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function releaseRetagShell(root, remote) {
|
|
378
|
+
return {
|
|
379
|
+
listReleasePackages: () => releasePackages(root),
|
|
380
|
+
resolveRef: (ref) => gitSha(root, ref),
|
|
381
|
+
resolveDispatchRef: (branch) => remoteBranchSha(root, remote, branch),
|
|
382
|
+
packageVersionAtRef: (packagePath, ref) => packageVersionAtRef(root, packagePath, ref),
|
|
383
|
+
npmVersionExists: (name, version) => npmVersionExists(root, name, version),
|
|
384
|
+
githubReleaseExists: (tag) => githubReleaseExists(root, tag),
|
|
385
|
+
remoteTagObject: (tag) => remoteReleaseTagObject(root, remote, tag),
|
|
386
|
+
createOrMoveTag: (tag, ref) => run('git', ['tag', '-fa', tag, '-m', tag, ref], root),
|
|
387
|
+
pushTags: (updates) => pushRetaggedReleaseTags(root, remote, updates),
|
|
388
|
+
dispatchPublishWorkflow: (workflow, branch) => run('gh', ['workflow', 'run', workflow, '--ref', branch, '-f', 'bump=auto', '-f', 'dry_run=false'], root),
|
|
389
|
+
log: (message) => console.log(message),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function listPendingReleaseTargets(root, ref) {
|
|
393
|
+
const head = await gitHead(root);
|
|
394
|
+
return pendingReleaseTargets(await listOwnedReleaseTagRecords(root, ref), head);
|
|
395
|
+
}
|
|
396
|
+
async function listOwnedReleaseTagRecords(root, ref) {
|
|
397
|
+
const tags = await gitReleaseTagsByCreatorDate(root);
|
|
398
|
+
console.log(`Repair pending releases: scanning ${tags.length} local tag${tags.length === 1 ? '' : 's'}.`);
|
|
399
|
+
return collectOwnedReleaseTagRecords(releasePackages(root), ref, {
|
|
400
|
+
listReleaseTagsByCreatorDate: async () => tags,
|
|
401
|
+
isAncestor: (ancestor, descendant) => gitIsAncestor(root, ancestor, descendant),
|
|
402
|
+
packageVersionAtRef: (packagePath, tagRef) => packageVersionAtRef(root, packagePath, tagRef),
|
|
403
|
+
durableTagState: (pkg, tag) => durableReleaseTagState(root, pkg, tag),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function durableReleaseTagState(root, pkg, tag) {
|
|
407
|
+
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
408
|
+
const start = Date.now();
|
|
409
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${tag}).`);
|
|
410
|
+
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
411
|
+
npmVersionExists(root, pkg.name, pkg.version),
|
|
412
|
+
githubReleaseExists(root, tag),
|
|
413
|
+
]);
|
|
414
|
+
console.log(`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`);
|
|
415
|
+
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
416
|
+
}
|
|
417
|
+
async function gitReleaseTagsByCreatorDate(root) {
|
|
418
|
+
const result = await $ `git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
419
|
+
.cwd(root)
|
|
420
|
+
.quiet()
|
|
421
|
+
.nothrow();
|
|
422
|
+
if (result.exitCode !== 0) {
|
|
423
|
+
throw new Error('Unable to list release tags by creator date.');
|
|
424
|
+
}
|
|
425
|
+
return decode(result.stdout)
|
|
426
|
+
.split('\n')
|
|
427
|
+
.map((line) => line.trim())
|
|
428
|
+
.filter(Boolean)
|
|
429
|
+
.map((line) => {
|
|
430
|
+
const [name, timestampText, peeledSha, objectSha] = line.split('\t');
|
|
431
|
+
const timestamp = Number(timestampText);
|
|
432
|
+
const sha = peeledSha || objectSha;
|
|
433
|
+
if (!name || !sha || !Number.isSafeInteger(timestamp)) {
|
|
434
|
+
throw new Error(`Unable to parse release tag ref: ${line}`);
|
|
435
|
+
}
|
|
436
|
+
return { name, sha, timestamp };
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function buildReleaseCandidate(root, packages) {
|
|
440
|
+
await run('nx', ['run-many', '-t', 'build', `--projects=${releasePackageProjects(packages)}`], root);
|
|
441
|
+
}
|
|
442
|
+
async function listMissingGithubReleasePackages(root, packages) {
|
|
443
|
+
return (await Promise.all(packages.map(async (pkg) => ((await githubReleaseExists(root, releaseTag(pkg))) ? null : pkg)))).filter((pkg) => pkg !== null);
|
|
444
|
+
}
|
|
445
|
+
async function createGithubRelease(root, pkg, dryRun) {
|
|
446
|
+
const currentTag = releaseTag(pkg);
|
|
447
|
+
console.log(`${pkg.name}@${pkg.version}: rendering GitHub Release notes for ${currentTag}.`);
|
|
448
|
+
console.log(`GitHub release auth: ${envPresence('GH_TOKEN')}, ${envPresence('GITHUB_TOKEN')}.`);
|
|
449
|
+
if (!dryRun) {
|
|
450
|
+
await assertRemoteTagExists(root, currentTag);
|
|
451
|
+
}
|
|
452
|
+
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
453
|
+
if (dryRun) {
|
|
454
|
+
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
458
|
+
await createOrUpdateGithubRelease(pkg, contents, {
|
|
459
|
+
githubReleaseExists: (tag) => githubReleaseExists(root, tag),
|
|
460
|
+
runGhRelease: (args) => run('gh', args, root),
|
|
461
|
+
log: (message) => console.log(message),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function envPresence(name) {
|
|
465
|
+
return `${name}=${process.env[name] ? 'present' : 'missing'}`;
|
|
466
|
+
}
|
|
467
|
+
function yesNo(value) {
|
|
468
|
+
return value ? 'yes' : 'no';
|
|
469
|
+
}
|
|
470
|
+
async function newerCommitsRemain(root) {
|
|
471
|
+
const branch = await releaseBranch(root);
|
|
472
|
+
const remote = await releaseRemote(root, branch);
|
|
473
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
474
|
+
const remoteRef = `${remote}/${branch}`;
|
|
475
|
+
if (!(await gitRefExists(root, remoteRef))) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const head = await gitHead(root);
|
|
479
|
+
return head !== (await gitSha(root, remoteRef)) && (await gitIsAncestor(root, head, remoteRef));
|
|
480
|
+
}
|
|
481
|
+
async function writeReleaseSummary(summary) {
|
|
482
|
+
const shortSha = summary.sha.slice(0, 12);
|
|
483
|
+
const lines = [
|
|
484
|
+
'## Release Summary',
|
|
485
|
+
'',
|
|
486
|
+
`- Commit: \`${shortSha}\``,
|
|
487
|
+
`- Mode: ${summary.dryRun ? 'dry run' : 'release'}`,
|
|
488
|
+
];
|
|
489
|
+
if (summary.noRelease) {
|
|
490
|
+
lines.push('- Result: no release tags found at HEAD');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
lines.push(`- Packages: ${packageSummary(summary.packages)}`);
|
|
494
|
+
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
495
|
+
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
496
|
+
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
497
|
+
lines.push(`- GitHub Releases created/updated: ${packageSummary(summary.githubReleases)}`);
|
|
498
|
+
}
|
|
499
|
+
if (summary.rerunRequired) {
|
|
500
|
+
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
501
|
+
console.log(`::warning::${message}`);
|
|
502
|
+
lines.push(`- Warning: ${message}`);
|
|
503
|
+
}
|
|
504
|
+
const text = `${lines.join('\n')}\n`;
|
|
505
|
+
console.log(text.trimEnd());
|
|
506
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
507
|
+
if (summaryPath) {
|
|
508
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function writeRepairSummary(summaries, dryRun) {
|
|
512
|
+
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
513
|
+
if (summaries.length === 0) {
|
|
514
|
+
lines.push('- Result: no pending releases needed repair');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
for (const summary of summaries) {
|
|
518
|
+
lines.push(`- Repaired \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const text = `${lines.join('\n')}\n`;
|
|
522
|
+
console.log(text.trimEnd());
|
|
523
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
524
|
+
if (summaryPath) {
|
|
525
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function packageSummary(packages) {
|
|
529
|
+
if (packages.length === 0) {
|
|
530
|
+
return 'none';
|
|
531
|
+
}
|
|
532
|
+
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
533
|
+
}
|
|
534
|
+
async function fetchReleaseRefs(root, remote, branch) {
|
|
535
|
+
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
536
|
+
}
|
|
537
|
+
async function gitRefExists(root, ref) {
|
|
538
|
+
const result = await $ `git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
|
|
539
|
+
return result.exitCode === 0;
|
|
540
|
+
}
|
|
541
|
+
async function gitIsAncestor(root, ancestor, descendant) {
|
|
542
|
+
const result = await $ `git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
|
|
543
|
+
return result.exitCode === 0;
|
|
544
|
+
}
|
|
545
|
+
async function gitSha(root, ref) {
|
|
546
|
+
const result = await $ `git rev-parse ${ref}`.cwd(root).quiet().nothrow();
|
|
547
|
+
if (result.exitCode !== 0) {
|
|
548
|
+
throw new Error(`Unable to resolve git ref ${ref}.`);
|
|
549
|
+
}
|
|
550
|
+
return decode(result.stdout).trim();
|
|
551
|
+
}
|
|
552
|
+
async function gitTagPointsAt(root, tag, ref) {
|
|
553
|
+
return (await gitCommitForTag(root, tag)) === (await gitSha(root, ref));
|
|
554
|
+
}
|
|
555
|
+
async function gitCommitForTag(root, tag) {
|
|
556
|
+
const result = await $ `git rev-list -n 1 ${tag}`.cwd(root).quiet().nothrow();
|
|
557
|
+
if (result.exitCode !== 0) {
|
|
558
|
+
throw new Error(`Unable to resolve release tag ${tag}.`);
|
|
559
|
+
}
|
|
560
|
+
return decode(result.stdout).trim();
|
|
561
|
+
}
|
|
562
|
+
async function remoteReleaseTagExists(root, remote, tag) {
|
|
563
|
+
const result = await $ `git ls-remote --exit-code --tags ${remote} ${`refs/tags/${tag}`}`.cwd(root).quiet().nothrow();
|
|
564
|
+
return result.exitCode === 0;
|
|
565
|
+
}
|
|
566
|
+
async function remoteReleaseTagObject(root, remote, tag) {
|
|
567
|
+
const result = await $ `git ls-remote --exit-code --tags ${remote} ${`refs/tags/${tag}`}`.cwd(root).quiet().nothrow();
|
|
568
|
+
if (result.exitCode === 2) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (result.exitCode !== 0) {
|
|
572
|
+
throw new Error(`Unable to inspect remote release tag ${tag} on ${remote}.`);
|
|
573
|
+
}
|
|
574
|
+
const [object] = decode(result.stdout).trim().split('\t');
|
|
575
|
+
if (!object) {
|
|
576
|
+
throw new Error(`Unable to parse remote release tag ${tag} on ${remote}.`);
|
|
577
|
+
}
|
|
578
|
+
return object;
|
|
579
|
+
}
|
|
580
|
+
async function remoteBranchSha(root, remote, branch) {
|
|
581
|
+
const result = await $ `git ls-remote --exit-code --heads ${remote} ${`refs/heads/${branch}`}`
|
|
582
|
+
.cwd(root)
|
|
583
|
+
.quiet()
|
|
584
|
+
.nothrow();
|
|
585
|
+
if (result.exitCode === 2) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
if (result.exitCode !== 0) {
|
|
589
|
+
throw new Error(`Unable to inspect remote branch ${branch} on ${remote}.`);
|
|
590
|
+
}
|
|
591
|
+
const [sha] = decode(result.stdout).trim().split('\t');
|
|
592
|
+
if (!sha) {
|
|
593
|
+
throw new Error(`Unable to parse remote branch ${branch} on ${remote}.`);
|
|
594
|
+
}
|
|
595
|
+
return sha;
|
|
596
|
+
}
|
|
597
|
+
async function pushRetaggedReleaseTags(root, remote, updates) {
|
|
598
|
+
const leases = updates.map((update) => `--force-with-lease=refs/tags/${update.tag}:${update.expectedRemoteObject ?? ''}`);
|
|
599
|
+
const refspecs = updates.map((update) => `refs/tags/${update.tag}:refs/tags/${update.tag}`);
|
|
600
|
+
await run('git', ['push', '--atomic', ...leases, remote, ...refspecs], root);
|
|
601
|
+
}
|
|
602
|
+
async function githubReleaseExists(root, tag) {
|
|
603
|
+
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
604
|
+
}
|
|
605
|
+
async function assertRemoteTagExists(root, tag) {
|
|
606
|
+
const branch = await releaseBranch(root);
|
|
607
|
+
const remote = await releaseRemote(root, branch);
|
|
608
|
+
if (!(await remoteReleaseTagExists(root, remote, tag))) {
|
|
609
|
+
throw new Error(`Release tag ${tag} is not present on remote ${remote}. Run smoo release version first.`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function npmVersionExists(root, name, version) {
|
|
613
|
+
const result = await $ `bun pm view ${`${name}@${version}`} version`.cwd(root).quiet().nothrow();
|
|
614
|
+
return result.exitCode === 0 && decode(result.stdout).trim() === version;
|
|
615
|
+
}
|
|
616
|
+
async function npmPackageExists(root, name) {
|
|
617
|
+
const result = await $ `bun pm view ${name} name`.cwd(root).quiet().nothrow();
|
|
618
|
+
return result.exitCode === 0;
|
|
619
|
+
}
|
|
175
620
|
async function assertCleanGitTree(root) {
|
|
176
621
|
const result = await $ `git status --porcelain --untracked-files=no`.cwd(root).quiet().nothrow();
|
|
177
622
|
if (result.exitCode !== 0) {
|
|
@@ -189,11 +634,6 @@ async function gitHead(root) {
|
|
|
189
634
|
}
|
|
190
635
|
return decode(result.stdout).trim();
|
|
191
636
|
}
|
|
192
|
-
async function pushReleaseCommit(root) {
|
|
193
|
-
const branch = await releaseBranch(root);
|
|
194
|
-
const remote = await releaseRemote(root, branch);
|
|
195
|
-
await run('git', ['push', '--follow-tags', '--atomic', remote, `HEAD:refs/heads/${branch}`], root);
|
|
196
|
-
}
|
|
197
637
|
async function releaseBranch(root) {
|
|
198
638
|
const githubBranch = process.env.GITHUB_REF_NAME?.trim();
|
|
199
639
|
if (githubBranch) {
|
|
@@ -242,18 +682,6 @@ function releaseBumpArg(bump = 'auto') {
|
|
|
242
682
|
}
|
|
243
683
|
return bump;
|
|
244
684
|
}
|
|
245
|
-
function releaseNpmTagArg(options) {
|
|
246
|
-
const bump = releaseBumpArg(options.bump);
|
|
247
|
-
const derivedTag = bump === 'prerelease' ? 'next' : 'latest';
|
|
248
|
-
const explicitTag = options.tag ?? options.npmTag;
|
|
249
|
-
if (!explicitTag) {
|
|
250
|
-
return derivedTag;
|
|
251
|
-
}
|
|
252
|
-
if (explicitTag !== derivedTag) {
|
|
253
|
-
throw new Error(`--bump ${bump} publishes with npm dist-tag ${derivedTag}, not ${explicitTag}.`);
|
|
254
|
-
}
|
|
255
|
-
return explicitTag;
|
|
256
|
-
}
|
|
257
685
|
function githubRepositoryFromRootPackage(root) {
|
|
258
686
|
const pkg = readPackageJson(join(root, 'package.json'));
|
|
259
687
|
const repository = pkg ? repositoryInfo(pkg.json) : null;
|
|
@@ -284,9 +712,16 @@ function githubRepositoryFromUrl(url) {
|
|
|
284
712
|
async function runLatestNpm(root, npmArgs, env) {
|
|
285
713
|
await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
286
714
|
}
|
|
287
|
-
async function runLatestNpmPublish(root, npmArgs) {
|
|
715
|
+
async function runLatestNpmPublish(root, npmArgs, useBootstrapToken) {
|
|
716
|
+
if (!useBootstrapToken) {
|
|
717
|
+
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
288
720
|
const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
|
|
289
721
|
if (!token || process.env.NPM_CONFIG_USERCONFIG) {
|
|
722
|
+
if (!token && !process.env.NPM_CONFIG_USERCONFIG) {
|
|
723
|
+
throw new Error('First publish for a package requires NODE_AUTH_TOKEN or NPM_TOKEN until trusted publishing exists.');
|
|
724
|
+
}
|
|
290
725
|
await runLatestNpm(root, npmArgs);
|
|
291
726
|
return;
|
|
292
727
|
}
|