@smoothbricks/cli 0.10.1 → 0.10.3
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 +25 -13
- package/dist/cli.js +24 -0
- package/dist/github-ci/index.d.ts +17 -2
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +145 -14
- package/dist/github-ci/outputs.d.ts +22 -0
- package/dist/github-ci/outputs.d.ts.map +1 -0
- package/dist/github-ci/outputs.js +649 -0
- package/dist/lib/conflict-markers.d.ts.map +1 -1
- package/dist/lib/workspace.d.ts +1 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +2 -1
- package/dist/monorepo/managed-files.d.ts +2 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +50 -5
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +26 -0
- package/dist/monorepo/packs/index.d.ts +3 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +5 -2
- package/dist/monorepo/publish-workflow.d.ts +1 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +240 -2
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +20 -7
- package/dist/nx/index.d.ts +6 -0
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +55 -5
- package/dist/release/github-release.d.ts +5 -2
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -0
- package/dist/release/index.d.ts +19 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +140 -31
- package/dist/release/orchestration.d.ts +8 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +38 -1
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +1 -1
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +1 -1
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +5 -0
- package/managed/raw/tooling/direnv/setup-environment.ts +32 -1
- package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
- package/managed/templates/github/actions/cache-node-modules/action.yml +3 -2
- package/managed/templates/github/actions/cache-nx/action.yml +2 -2
- package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
- package/package.json +7 -14
- package/src/cli.ts +30 -4
- package/src/github-ci/index.test.ts +486 -0
- package/src/github-ci/index.ts +177 -16
- package/src/github-ci/outputs.ts +506 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +13 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +234 -5
- package/src/monorepo/ci-workflow.ts +3 -1
- package/src/monorepo/managed-files.test.ts +75 -0
- package/src/monorepo/managed-files.ts +61 -5
- package/src/monorepo/package-policy.test.ts +57 -38
- package/src/monorepo/package-policy.ts +28 -0
- package/src/monorepo/packs/index.test.ts +18 -1
- package/src/monorepo/packs/index.ts +7 -3
- package/src/monorepo/publish-workflow.ts +481 -2
- package/src/monorepo/tool-validation.test.ts +59 -24
- package/src/monorepo/tool-validation.ts +21 -7
- package/src/monorepo/wrangler.test.ts +9 -2
- package/src/nx/index.test.ts +39 -0
- package/src/nx/index.ts +65 -5
- package/src/release/__tests__/fixture-repo.test.ts +18 -16
- package/src/release/__tests__/github-release.test.ts +11 -0
- package/src/release/__tests__/orchestration.test.ts +57 -2
- package/src/release/__tests__/trust-publisher.test.ts +91 -2
- package/src/release/github-release.ts +12 -0
- package/src/release/index.ts +218 -40
- package/src/release/orchestration.ts +55 -3
- package/src/wrangler/prepare-env.ts +14 -4
- package/src/wrangler/scaffold.ts +8 -3
|
@@ -3,8 +3,11 @@ import type { BootstrapNpmPackagesOptions } from '../bootstrap-npm-packages.js';
|
|
|
3
3
|
import type { ReleasePackageInfo } from '../core.js';
|
|
4
4
|
import {
|
|
5
5
|
configureTrustedPublishers,
|
|
6
|
+
npmTrustGithubArgs,
|
|
7
|
+
npmTrustListAccessDenied,
|
|
6
8
|
parseTrustedPublishers,
|
|
7
9
|
type TrustedPublisher,
|
|
10
|
+
type TrustedPublisherLookup,
|
|
8
11
|
type TrustPublisherShell,
|
|
9
12
|
} from '../index.js';
|
|
10
13
|
|
|
@@ -101,6 +104,84 @@ describe('trusted publisher setup', () => {
|
|
|
101
104
|
expect(shell.events).toEqual([`exists:${missing.name}`, `list:${missing.name}`, `trust:${missing.name}:false`]);
|
|
102
105
|
});
|
|
103
106
|
|
|
107
|
+
it('reports package owners and opens npm login after trust access is denied', async () => {
|
|
108
|
+
const shell = new RecordingTrustPublisherShell({
|
|
109
|
+
packages: [stable],
|
|
110
|
+
existing: [stable.name],
|
|
111
|
+
trustedPublisherResponses: {
|
|
112
|
+
[stable.name]: [
|
|
113
|
+
{
|
|
114
|
+
status: 'access-denied',
|
|
115
|
+
identity: 'current-user',
|
|
116
|
+
owners: 'package-owner <owner@example.test>',
|
|
117
|
+
},
|
|
118
|
+
[],
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await configureTrustedPublishers(shell, {});
|
|
124
|
+
|
|
125
|
+
expect(shell.events).toEqual([
|
|
126
|
+
`exists:${stable.name}`,
|
|
127
|
+
`list:${stable.name}`,
|
|
128
|
+
'login',
|
|
129
|
+
`list:${stable.name}`,
|
|
130
|
+
`trust:${stable.name}:false`,
|
|
131
|
+
]);
|
|
132
|
+
expect(shell.errors.join('\n')).toContain('npm account "current-user"');
|
|
133
|
+
expect(shell.errors.join('\n')).toContain('package-owner <owner@example.test>');
|
|
134
|
+
expect(shell.logs).toContain(
|
|
135
|
+
`${stable.name}: opening npm browser login. Sign in as a listed package owner, then return here.`,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('stops after one owner login when the selected account still lacks access', async () => {
|
|
140
|
+
const shell = new RecordingTrustPublisherShell({
|
|
141
|
+
packages: [stable],
|
|
142
|
+
existing: [stable.name],
|
|
143
|
+
trustedPublisherResponses: {
|
|
144
|
+
[stable.name]: [
|
|
145
|
+
{
|
|
146
|
+
status: 'access-denied',
|
|
147
|
+
identity: 'current-user',
|
|
148
|
+
owners: 'package-owner <owner@example.test>',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
status: 'access-denied',
|
|
152
|
+
identity: 'wrong-user',
|
|
153
|
+
owners: 'package-owner <owner@example.test>',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await expect(configureTrustedPublishers(shell, {})).rejects.toThrow(
|
|
160
|
+
'npm browser login completed, but the selected account still cannot manage this package',
|
|
161
|
+
);
|
|
162
|
+
expect(shell.events).toEqual([`exists:${stable.name}`, `list:${stable.name}`, 'login', `list:${stable.name}`]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('grants the trusted workflow permission to run npm publish', () => {
|
|
166
|
+
expect(npmTrustGithubArgs(stable.name, 'scope/repo', 'publish.yml', false)).toEqual([
|
|
167
|
+
'trust',
|
|
168
|
+
'github',
|
|
169
|
+
stable.name,
|
|
170
|
+
'--file',
|
|
171
|
+
'publish.yml',
|
|
172
|
+
'--repo',
|
|
173
|
+
'scope/repo',
|
|
174
|
+
'--allow-publish',
|
|
175
|
+
'--yes',
|
|
176
|
+
]);
|
|
177
|
+
expect(npmTrustGithubArgs(stable.name, 'scope/repo', 'publish.yml', true).at(-1)).toBe('--dry-run');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('recognizes npm trust access-denied output', () => {
|
|
181
|
+
expect(npmTrustListAccessDenied('{"error":{"code":"E403"}}', '')).toBe(true);
|
|
182
|
+
expect(npmTrustListAccessDenied('', 'npm error code EOTP')).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
104
185
|
it('parses empty npm trust list output as no trusted publishers', () => {
|
|
105
186
|
expect(parseTrustedPublishers('', stable.name)).toEqual([]);
|
|
106
187
|
expect(parseTrustedPublishers('null', stable.name)).toEqual([]);
|
|
@@ -116,15 +197,18 @@ class RecordingTrustPublisherShell implements TrustPublisherShell<ReleasePackage
|
|
|
116
197
|
private readonly packages: ReleasePackageInfo[];
|
|
117
198
|
private readonly existing: Set<string>;
|
|
118
199
|
private readonly trustedPublisherByPackage: Record<string, TrustedPublisher[]>;
|
|
200
|
+
private readonly trustedPublisherResponses: Record<string, TrustedPublisherLookup[]>;
|
|
119
201
|
|
|
120
202
|
constructor(options: {
|
|
121
203
|
packages: ReleasePackageInfo[];
|
|
122
204
|
existing: string[];
|
|
123
205
|
trustedPublishers?: Record<string, TrustedPublisher[]>;
|
|
206
|
+
trustedPublisherResponses?: Record<string, TrustedPublisherLookup[]>;
|
|
124
207
|
}) {
|
|
125
208
|
this.packages = options.packages;
|
|
126
209
|
this.existing = new Set(options.existing);
|
|
127
210
|
this.trustedPublisherByPackage = options.trustedPublishers ?? {};
|
|
211
|
+
this.trustedPublisherResponses = options.trustedPublisherResponses ?? {};
|
|
128
212
|
}
|
|
129
213
|
|
|
130
214
|
listReleasePackages(): ReleasePackageInfo[] {
|
|
@@ -145,14 +229,19 @@ class RecordingTrustPublisherShell implements TrustPublisherShell<ReleasePackage
|
|
|
145
229
|
return missingPackages;
|
|
146
230
|
}
|
|
147
231
|
|
|
232
|
+
async login(): Promise<void> {
|
|
233
|
+
this.events.push('login');
|
|
234
|
+
}
|
|
235
|
+
|
|
148
236
|
async trustPublisher(pkg: ReleasePackageInfo, dryRun: boolean): Promise<'configured' | 'already-configured'> {
|
|
149
237
|
this.events.push(`trust:${pkg.name}:${dryRun}`);
|
|
150
238
|
return 'configured';
|
|
151
239
|
}
|
|
152
240
|
|
|
153
|
-
async trustedPublishers(pkg: ReleasePackageInfo): Promise<
|
|
241
|
+
async trustedPublishers(pkg: ReleasePackageInfo): Promise<TrustedPublisherLookup> {
|
|
154
242
|
this.events.push(`list:${pkg.name}`);
|
|
155
|
-
|
|
243
|
+
const response = this.trustedPublisherResponses[pkg.name]?.shift();
|
|
244
|
+
return response ?? this.trustedPublisherByPackage[pkg.name] ?? [];
|
|
156
245
|
}
|
|
157
246
|
|
|
158
247
|
log(message: string): void {
|
|
@@ -71,6 +71,7 @@ export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: str
|
|
|
71
71
|
gitPush: false,
|
|
72
72
|
stageChanges: false,
|
|
73
73
|
createRelease: false,
|
|
74
|
+
forceChangelogGeneration: true,
|
|
74
75
|
deleteVersionPlans: false,
|
|
75
76
|
dryRun,
|
|
76
77
|
} satisfies ChangelogOptions;
|
|
@@ -85,6 +86,17 @@ export function projectChangelogContents(result: ProjectChangelogLookupResult, p
|
|
|
85
86
|
return changelog.contents;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export function githubReleaseLookupExists(tag: string, exitCode: number, stdout: string, stderr: string): boolean {
|
|
90
|
+
if (exitCode === 0) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join('\n');
|
|
94
|
+
if (/\bHTTP 404\b|release not found/i.test(details)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Unable to inspect GitHub Release ${tag}.${details ? `\n${details}` : ''}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
export function githubReleaseCommandArgs(
|
|
89
101
|
tag: string,
|
|
90
102
|
notesFile: string,
|
package/src/release/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, resolve } 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 { githubCiApplyOutputs, githubCiNxRunMany } from '../github-ci/index.js';
|
|
7
8
|
import { assertNoConflictMarkers } from '../lib/conflict-markers.js';
|
|
8
9
|
import { withDevenvEnv } from '../lib/devenv.js';
|
|
9
10
|
import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
|
|
10
|
-
import { decode, run, runInteractiveStatus, runResult
|
|
11
|
+
import { decode, run, runInteractiveStatus, runResult } from '../lib/run.js';
|
|
11
12
|
import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
|
|
12
13
|
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
13
14
|
import {
|
|
@@ -29,14 +30,18 @@ import {
|
|
|
29
30
|
} from './core.js';
|
|
30
31
|
import {
|
|
31
32
|
createOrUpdateGithubRelease,
|
|
33
|
+
githubReleaseLookupExists,
|
|
32
34
|
renderNxProjectChangelogContents,
|
|
33
35
|
withNxWorkspaceRoot,
|
|
34
36
|
} from './github-release.js';
|
|
35
37
|
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
36
38
|
import {
|
|
37
39
|
bumpStableReleaseToNext,
|
|
40
|
+
collectRepairPlatformOutputs,
|
|
38
41
|
completeReleaseAtHead as completeReleaseAtHeadWithShell,
|
|
39
42
|
type ReleaseCompletionShell,
|
|
43
|
+
type ReleaseRepairOutputsShell,
|
|
44
|
+
type ReleaseRepairShell,
|
|
40
45
|
type ReleaseSummary,
|
|
41
46
|
type ReleaseVersionMode,
|
|
42
47
|
repairPendingTargets,
|
|
@@ -57,6 +62,15 @@ export interface ReleasePublishOptions {
|
|
|
57
62
|
|
|
58
63
|
export interface ReleaseRepairPendingOptions {
|
|
59
64
|
dryRun?: boolean;
|
|
65
|
+
platformOutputs?: string;
|
|
66
|
+
ref?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ReleasePlatformOutputsOptions {
|
|
70
|
+
bump: string;
|
|
71
|
+
output: string;
|
|
72
|
+
ref?: string;
|
|
73
|
+
targets: string;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
export interface ReleaseTrustPublisherOptions {
|
|
@@ -162,26 +176,56 @@ export async function releasePublish(root: string, options: ReleasePublishOption
|
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
export async function releaseRepairPending(root: string, options: ReleaseRepairPendingOptions): Promise<void> {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
console.log(`Repair pending releases:
|
|
168
|
-
await
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
|
|
179
|
+
const repairRef = await fetchReleaseRepairRef(root, options.ref);
|
|
180
|
+
const restoreRef = options.ref ? await gitHead(root) : repairRef;
|
|
181
|
+
console.log(`Repair pending releases: planning from ${repairRef}.`);
|
|
182
|
+
const targets = await listPendingReleaseTargets(root, repairRef);
|
|
183
|
+
logPendingReleaseTargets(targets);
|
|
184
|
+
const platformOutputs = options.platformOutputs ? resolve(root, options.platformOutputs) : undefined;
|
|
185
|
+
const summaries = await repairPendingTargets(
|
|
186
|
+
releaseRepairShell(root, platformOutputs),
|
|
187
|
+
targets,
|
|
188
|
+
restoreRef,
|
|
189
|
+
options.dryRun === true,
|
|
177
190
|
);
|
|
178
|
-
for (const target of targets) {
|
|
179
|
-
console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
|
|
180
|
-
}
|
|
181
|
-
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
182
191
|
await writeRepairSummary(summaries, options.dryRun === true);
|
|
183
192
|
}
|
|
184
193
|
|
|
194
|
+
export async function releaseCollectPlatformOutputs(
|
|
195
|
+
root: string,
|
|
196
|
+
options: ReleasePlatformOutputsOptions,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const restoreRef = await gitHead(root);
|
|
199
|
+
const releaseRef = await fetchReleaseRepairRef(root, options.ref);
|
|
200
|
+
const packages = releasePackages(root);
|
|
201
|
+
const packagesAtHead = await releasePackagesAtHead(root, packages);
|
|
202
|
+
const currentPackages =
|
|
203
|
+
packagesAtHead.length > 0
|
|
204
|
+
? packagesAtHead
|
|
205
|
+
: await releasePlatformPackages(root, packages, releaseBumpArg(options.bump));
|
|
206
|
+
console.log(
|
|
207
|
+
currentPackages.length === 0
|
|
208
|
+
? 'Release platform outputs: no current release packages selected.'
|
|
209
|
+
: `Release platform outputs: building current outputs for ${packageSummary(currentPackages)}.`,
|
|
210
|
+
);
|
|
211
|
+
const outputRoot = resolve(root, options.output);
|
|
212
|
+
await githubCiNxRunMany(root, {
|
|
213
|
+
targets: options.targets,
|
|
214
|
+
projects: releasePackageProjects(currentPackages),
|
|
215
|
+
collectOutputs: join(outputRoot, 'current'),
|
|
216
|
+
allowEmptyProjects: true,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log(`Repair platform outputs: planning from ${releaseRef}.`);
|
|
220
|
+
const targets = await listPendingReleaseTargets(root, releaseRef);
|
|
221
|
+
logPendingReleaseTargets(targets);
|
|
222
|
+
await collectRepairPlatformOutputs(
|
|
223
|
+
releaseRepairOutputsShell(root, options.targets, join(outputRoot, 'repairs')),
|
|
224
|
+
targets,
|
|
225
|
+
restoreRef,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
185
229
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
186
230
|
const repository = githubRepositoryFromRootPackage(root);
|
|
187
231
|
const workflow = 'publish.yml';
|
|
@@ -203,14 +247,10 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
|
|
|
203
247
|
},
|
|
204
248
|
bootstrapOptions,
|
|
205
249
|
),
|
|
206
|
-
trustPublisher: (pkg, dryRun, env) =>
|
|
207
|
-
|
|
208
|
-
if (dryRun) {
|
|
209
|
-
args.push('--dry-run');
|
|
210
|
-
}
|
|
211
|
-
return runLatestNpmTrust(root, args, env);
|
|
212
|
-
},
|
|
250
|
+
trustPublisher: (pkg, dryRun, env) =>
|
|
251
|
+
runLatestNpmTrust(root, npmTrustGithubArgs(pkg.name, repository, workflow, dryRun), env),
|
|
213
252
|
trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
|
|
253
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
214
254
|
log: (message) => console.log(message),
|
|
215
255
|
error: (message) => console.error(message),
|
|
216
256
|
},
|
|
@@ -218,6 +258,19 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
|
|
|
218
258
|
);
|
|
219
259
|
}
|
|
220
260
|
|
|
261
|
+
export function npmTrustGithubArgs(
|
|
262
|
+
packageName: string,
|
|
263
|
+
repository: string,
|
|
264
|
+
workflow: string,
|
|
265
|
+
dryRun: boolean,
|
|
266
|
+
): string[] {
|
|
267
|
+
const args = ['trust', 'github', packageName, '--file', workflow, '--repo', repository, '--allow-publish', '--yes'];
|
|
268
|
+
if (dryRun) {
|
|
269
|
+
args.push('--dry-run');
|
|
270
|
+
}
|
|
271
|
+
return args;
|
|
272
|
+
}
|
|
273
|
+
|
|
221
274
|
export interface TrustPublisherShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
222
275
|
repository: string;
|
|
223
276
|
workflow: string;
|
|
@@ -225,7 +278,8 @@ export interface TrustPublisherShell<Package extends ReleasePackageInfo = Releas
|
|
|
225
278
|
packageExists(name: string): Promise<boolean>;
|
|
226
279
|
bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
|
|
227
280
|
trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
|
|
228
|
-
trustedPublishers(pkg: Package): Promise<
|
|
281
|
+
trustedPublishers(pkg: Package): Promise<TrustedPublisherLookup>;
|
|
282
|
+
login(): Promise<void>;
|
|
229
283
|
log(message: string): void;
|
|
230
284
|
error(message: string): void;
|
|
231
285
|
}
|
|
@@ -239,6 +293,14 @@ export interface TrustedPublisher {
|
|
|
239
293
|
repository?: string;
|
|
240
294
|
}
|
|
241
295
|
|
|
296
|
+
export interface NpmTrustAccessDenied {
|
|
297
|
+
readonly status: 'access-denied';
|
|
298
|
+
readonly identity: string;
|
|
299
|
+
readonly owners: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export type TrustedPublisherLookup = TrustedPublisher[] | NpmTrustAccessDenied;
|
|
303
|
+
|
|
242
304
|
export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
|
|
243
305
|
shell: TrustPublisherShell<Package>,
|
|
244
306
|
options: ReleaseTrustPublisherOptions,
|
|
@@ -275,7 +337,7 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
|
|
|
275
337
|
const failedPackages: string[] = [];
|
|
276
338
|
for (const pkg of selectedPackages) {
|
|
277
339
|
if (!options.dryRun) {
|
|
278
|
-
const trustedPublishers = await shell
|
|
340
|
+
const trustedPublishers = await trustedPublishersWithOwnerLogin(shell, pkg);
|
|
279
341
|
if (hasMatchingGithubTrustedPublisher(trustedPublishers, shell.repository, shell.workflow)) {
|
|
280
342
|
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
281
343
|
continue;
|
|
@@ -315,6 +377,33 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
|
|
|
315
377
|
}
|
|
316
378
|
}
|
|
317
379
|
|
|
380
|
+
async function trustedPublishersWithOwnerLogin<Package extends ReleasePackageInfo>(
|
|
381
|
+
shell: TrustPublisherShell<Package>,
|
|
382
|
+
pkg: Package,
|
|
383
|
+
): Promise<TrustedPublisher[]> {
|
|
384
|
+
let result = await shell.trustedPublishers(pkg);
|
|
385
|
+
if (Array.isArray(result)) {
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
shell.error(
|
|
390
|
+
`${pkg.name}: npm trust access denied for npm account "${result.identity}". ` +
|
|
391
|
+
`npm trust requires package write access. Package owners:\n${result.owners}`,
|
|
392
|
+
);
|
|
393
|
+
shell.log(`${pkg.name}: opening npm browser login. Sign in as a listed package owner, then return here.`);
|
|
394
|
+
await shell.login();
|
|
395
|
+
|
|
396
|
+
result = await shell.trustedPublishers(pkg);
|
|
397
|
+
if (Array.isArray(result)) {
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
throw new Error(
|
|
401
|
+
`${pkg.name}: npm trust access denied for npm account "${result.identity}". ` +
|
|
402
|
+
`npm trust requires package write access. Package owners:\n${result.owners}\n` +
|
|
403
|
+
'npm browser login completed, but the selected account still cannot manage this package.',
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
318
407
|
function selectedTrustPublisherPackages<Package extends ReleasePackageInfo>(
|
|
319
408
|
packages: Package[],
|
|
320
409
|
selections: string[],
|
|
@@ -548,6 +637,19 @@ async function releaseVersionPackages(
|
|
|
548
637
|
);
|
|
549
638
|
}
|
|
550
639
|
|
|
640
|
+
async function releasePlatformPackages(
|
|
641
|
+
root: string,
|
|
642
|
+
packages: ReleasePackage[],
|
|
643
|
+
bump: string,
|
|
644
|
+
): Promise<ReleasePackage[]> {
|
|
645
|
+
const candidates = await releaseVersionPackages(root, packages, bump);
|
|
646
|
+
if (candidates.length === 0) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
// Nx may version additional reverse dependents, so use its dry-run result rather than only the direct candidates.
|
|
650
|
+
return runNxReleaseVersionPreview(root, releasePackageProjects(candidates), bump);
|
|
651
|
+
}
|
|
652
|
+
|
|
551
653
|
async function runNxReleaseVersion(
|
|
552
654
|
root: string,
|
|
553
655
|
projects: string,
|
|
@@ -603,6 +705,7 @@ async function runNxReleaseVersionPreview(root: string, projects: string, bump:
|
|
|
603
705
|
gitPush: false,
|
|
604
706
|
stageChanges: false,
|
|
605
707
|
createRelease: false,
|
|
708
|
+
forceChangelogGeneration: true,
|
|
606
709
|
dryRun: true,
|
|
607
710
|
});
|
|
608
711
|
releasePreviewChangelogs.clear();
|
|
@@ -808,17 +911,11 @@ function releaseNextShell(root: string): { bumpStablePackagesToNext(packages: Re
|
|
|
808
911
|
};
|
|
809
912
|
}
|
|
810
913
|
|
|
811
|
-
function
|
|
812
|
-
checkout(ref: string): Promise<void>;
|
|
813
|
-
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
814
|
-
beforeRepairTarget(target: ReleaseTarget): void;
|
|
815
|
-
afterRepairTarget(target: ReleaseTarget): void;
|
|
816
|
-
} {
|
|
914
|
+
function releaseTargetCheckoutShell(root: string) {
|
|
817
915
|
return {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
beforeRepairTarget: (target) => {
|
|
916
|
+
checkout: (ref: string) => run('git', ['switch', '--detach', ref], root),
|
|
917
|
+
withDevenvEnv: <T>(runWithEnv: () => Promise<T>) => withDevenvEnv(root, runWithEnv),
|
|
918
|
+
beforeRepairTarget: (target: ReleaseTarget) => {
|
|
822
919
|
console.log(`::group::Repair pending release ${target.sha.slice(0, 12)} (${packageSummary(target.packages)})`);
|
|
823
920
|
},
|
|
824
921
|
afterRepairTarget: () => {
|
|
@@ -827,6 +924,37 @@ function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage
|
|
|
827
924
|
};
|
|
828
925
|
}
|
|
829
926
|
|
|
927
|
+
function releaseRepairShell(root: string, platformOutputs?: string): ReleaseRepairShell<ReleasePackage> {
|
|
928
|
+
return {
|
|
929
|
+
...releaseCompletionShell(root),
|
|
930
|
+
...releaseTargetCheckoutShell(root),
|
|
931
|
+
prepareRepairTarget: async (target) => {
|
|
932
|
+
if (!platformOutputs || target.npmPackages.length === 0) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const output = join(platformOutputs, target.sha);
|
|
936
|
+
console.log(`Repair pending releases: applying cross-platform outputs from ${output}.`);
|
|
937
|
+
await githubCiApplyOutputs(root, [output], target.sha);
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function releaseRepairOutputsShell(
|
|
943
|
+
root: string,
|
|
944
|
+
targetGlobs: string,
|
|
945
|
+
outputRoot: string,
|
|
946
|
+
): ReleaseRepairOutputsShell<ReleasePackage> {
|
|
947
|
+
return {
|
|
948
|
+
...releaseTargetCheckoutShell(root),
|
|
949
|
+
collectRepairTargetOutputs: (target) =>
|
|
950
|
+
githubCiNxRunMany(root, {
|
|
951
|
+
targets: targetGlobs,
|
|
952
|
+
projects: releasePackageProjects(target.npmPackages),
|
|
953
|
+
collectOutputs: join(outputRoot, target.sha),
|
|
954
|
+
}),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
830
958
|
function releaseRetagShell(root: string, remote: string) {
|
|
831
959
|
return {
|
|
832
960
|
listReleasePackages: () => releasePackages(root),
|
|
@@ -844,6 +972,29 @@ function releaseRetagShell(root: string, remote: string) {
|
|
|
844
972
|
};
|
|
845
973
|
}
|
|
846
974
|
|
|
975
|
+
async function fetchReleaseRepairRef(root: string, requestedRef?: string): Promise<string> {
|
|
976
|
+
const branch = await releaseBranch(root);
|
|
977
|
+
const remote = await releaseRemote(root, branch);
|
|
978
|
+
console.log(`Repair pending releases: fetching ${remote}/${branch} and tags.`);
|
|
979
|
+
await fetchReleaseRefs(root, remote, branch);
|
|
980
|
+
if (requestedRef) {
|
|
981
|
+
return requestedRef;
|
|
982
|
+
}
|
|
983
|
+
const remoteRef = `${remote}/${branch}`;
|
|
984
|
+
return (await gitRefExists(root, remoteRef)) ? remoteRef : gitHead(root);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function logPendingReleaseTargets(targets: ReleaseTarget[]): void {
|
|
988
|
+
console.log(
|
|
989
|
+
targets.length === 0
|
|
990
|
+
? 'Repair pending releases: no pending durable state repairs found.'
|
|
991
|
+
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
|
|
992
|
+
);
|
|
993
|
+
for (const target of targets) {
|
|
994
|
+
console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
847
998
|
async function listPendingReleaseTargets(root: string, ref: string): Promise<ReleaseTarget[]> {
|
|
848
999
|
const head = await gitHead(root);
|
|
849
1000
|
return pendingReleaseTargets(await listOwnedReleaseTagRecords(root, ref), head);
|
|
@@ -1069,7 +1220,8 @@ function repairReasons(target: ReleaseTarget, pkg: ReleasePackage): string[] {
|
|
|
1069
1220
|
}
|
|
1070
1221
|
|
|
1071
1222
|
async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
|
|
1072
|
-
|
|
1223
|
+
// `retag-unpublished` intentionally moves repairable tags, so the remote tag is authoritative.
|
|
1224
|
+
await run('git', ['fetch', '--force', '--tags', remote, branch], root);
|
|
1073
1225
|
}
|
|
1074
1226
|
|
|
1075
1227
|
async function gitRefExists(root: string, ref: string): Promise<boolean> {
|
|
@@ -1309,7 +1461,8 @@ async function pushRetaggedReleaseTags(
|
|
|
1309
1461
|
}
|
|
1310
1462
|
|
|
1311
1463
|
async function githubReleaseExists(root: string, tag: string): Promise<boolean> {
|
|
1312
|
-
|
|
1464
|
+
const result = await $`gh release view ${tag} --json tagName`.cwd(root).quiet().nothrow();
|
|
1465
|
+
return githubReleaseLookupExists(tag, result.exitCode, decode(result.stdout), decode(result.stderr));
|
|
1313
1466
|
}
|
|
1314
1467
|
|
|
1315
1468
|
function githubReleaseUrl(root: string, tag: string): string {
|
|
@@ -1465,7 +1618,7 @@ async function runLatestNpmTrust(
|
|
|
1465
1618
|
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
|
|
1466
1619
|
}
|
|
1467
1620
|
|
|
1468
|
-
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<
|
|
1621
|
+
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisherLookup> {
|
|
1469
1622
|
const args = ['trust', 'list', pkg.name, '--json'];
|
|
1470
1623
|
let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1471
1624
|
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
@@ -1475,6 +1628,9 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1475
1628
|
}
|
|
1476
1629
|
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1477
1630
|
}
|
|
1631
|
+
if (result.exitCode !== 0 && npmTrustListAccessDenied(result.stdout, result.stderr)) {
|
|
1632
|
+
return npmTrustAccessDetails(root, pkg.name);
|
|
1633
|
+
}
|
|
1478
1634
|
if (result.exitCode !== 0) {
|
|
1479
1635
|
throw new Error(
|
|
1480
1636
|
`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
|
|
@@ -1483,6 +1639,28 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1483
1639
|
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1484
1640
|
}
|
|
1485
1641
|
|
|
1642
|
+
export function npmTrustListAccessDenied(stdout: string, stderr: string): boolean {
|
|
1643
|
+
return /\bE403\b/.test(`${stdout}\n${stderr}`);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async function npmTrustAccessDetails(root: string, packageName: string): Promise<NpmTrustAccessDenied> {
|
|
1647
|
+
const [identityResult, ownersResult] = await Promise.all([
|
|
1648
|
+
runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', 'whoami'], root),
|
|
1649
|
+
runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', 'owner', 'ls', packageName], root),
|
|
1650
|
+
]);
|
|
1651
|
+
return {
|
|
1652
|
+
status: 'access-denied',
|
|
1653
|
+
identity:
|
|
1654
|
+
identityResult.exitCode === 0 && identityResult.stdout.trim().length > 0
|
|
1655
|
+
? identityResult.stdout.trim()
|
|
1656
|
+
: '(not authenticated)',
|
|
1657
|
+
owners:
|
|
1658
|
+
ownersResult.exitCode === 0 && ownersResult.stdout.trim().length > 0
|
|
1659
|
+
? ownersResult.stdout.trim()
|
|
1660
|
+
: '(owner lookup unavailable)',
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1486
1664
|
export function parseTrustedPublishers(stdout: string, packageName: string): TrustedPublisher[] {
|
|
1487
1665
|
if (stdout.trim().length === 0) {
|
|
1488
1666
|
return [];
|
|
@@ -30,14 +30,24 @@ export interface ReleaseNextShell<Package extends ReleasePackageInfo = ReleasePa
|
|
|
30
30
|
bumpStablePackagesToNext(packages: Package[]): Promise<void>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export interface
|
|
34
|
-
extends ReleaseCompletionShell<Package> {
|
|
33
|
+
export interface ReleaseTargetCheckoutShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
35
34
|
checkout(ref: string): Promise<void>;
|
|
36
35
|
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
37
36
|
beforeRepairTarget?(target: ReleaseTarget<Package>): void;
|
|
38
37
|
afterRepairTarget?(target: ReleaseTarget<Package>): void;
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
|
|
41
|
+
extends ReleaseCompletionShell<Package>,
|
|
42
|
+
ReleaseTargetCheckoutShell<Package> {
|
|
43
|
+
prepareRepairTarget?(target: ReleaseTarget<Package>): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ReleaseRepairOutputsShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
|
|
47
|
+
extends ReleaseTargetCheckoutShell<Package> {
|
|
48
|
+
collectRepairTargetOutputs(target: ReleaseTarget<Package>): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
42
52
|
releasePackagesAtHead(): Promise<Package[]>;
|
|
43
53
|
releaseVersionPackages(bump: string): Promise<Package[]>;
|
|
@@ -151,17 +161,59 @@ export async function repairPendingTargets<Package extends ReleasePackageInfo>(
|
|
|
151
161
|
}
|
|
152
162
|
shell.beforeRepairTarget?.(target);
|
|
153
163
|
try {
|
|
154
|
-
summaries.push(
|
|
164
|
+
summaries.push(
|
|
165
|
+
await shell.withDevenvEnv(async () => {
|
|
166
|
+
await shell.prepareRepairTarget?.(target);
|
|
167
|
+
return completeRepairTargetAtHead(shell, target, dryRun);
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
155
170
|
} finally {
|
|
156
171
|
shell.afterRepairTarget?.(target);
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
} finally {
|
|
160
175
|
await shell.checkout(restoreRef);
|
|
176
|
+
if (targets.length > 0) {
|
|
177
|
+
// Loading devenv at a historical release can rebuild ignored tool outputs
|
|
178
|
+
// from that checkout. Refresh after restoring HEAD so later release phases
|
|
179
|
+
// cannot combine the current CLI with an older Nx plugin build.
|
|
180
|
+
await shell.withDevenvEnv(async () => undefined);
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
return summaries;
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
export async function collectRepairPlatformOutputs<Package extends ReleasePackageInfo>(
|
|
187
|
+
shell: ReleaseRepairOutputsShell<Package>,
|
|
188
|
+
targets: Array<ReleaseTarget<Package>>,
|
|
189
|
+
restoreRef: string,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const outputTargets = targets.filter((target) => target.npmPackages.length > 0);
|
|
192
|
+
if (outputTargets.length === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
for (const target of outputTargets) {
|
|
197
|
+
await shell.checkout(target.sha);
|
|
198
|
+
if (target.packages.length === 0) {
|
|
199
|
+
// invariant throw: release target discovery must never yield a package-free target.
|
|
200
|
+
throw new Error(`Release commit ${target.sha} has no release packages after checkout.`);
|
|
201
|
+
}
|
|
202
|
+
shell.beforeRepairTarget?.(target);
|
|
203
|
+
try {
|
|
204
|
+
await shell.withDevenvEnv(() => shell.collectRepairTargetOutputs(target));
|
|
205
|
+
} finally {
|
|
206
|
+
shell.afterRepairTarget?.(target);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await shell.checkout(restoreRef);
|
|
211
|
+
// Historical devenv activation may rebuild ignored tool outputs. Restore the
|
|
212
|
+
// dispatch environment before the macOS job performs any further work.
|
|
213
|
+
await shell.withDevenvEnv(async () => undefined);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
165
217
|
function repairDryRunSummary<Package extends ReleasePackageInfo>(
|
|
166
218
|
target: ReleaseTarget<Package>,
|
|
167
219
|
): ReleaseSummary<Package> {
|