@smoothbricks/cli 0.10.7 → 0.10.9
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/github-ci/index.d.ts +44 -4
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +220 -34
- package/dist/monorepo/ci-workflow.js +16 -6
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +19 -1
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
- package/dist/monorepo/publish-workflow.js +3 -3
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +85 -5
- package/dist/playwright/index.d.ts +22 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +44 -0
- package/dist/release/bootstrap-npm-packages.d.ts +3 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
- package/dist/release/bootstrap-npm-packages.js +21 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +31 -6
- package/dist/wrangler/cloudflare.d.ts +87 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -0
- package/dist/wrangler/cloudflare.js +238 -0
- package/dist/wrangler/deploy-environment.d.ts +48 -0
- package/dist/wrangler/deploy-environment.d.ts.map +1 -0
- package/dist/wrangler/deploy-environment.js +383 -0
- package/dist/wrangler/environment.d.ts +58 -0
- package/dist/wrangler/environment.d.ts.map +1 -0
- package/dist/wrangler/environment.js +297 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
- package/package.json +9 -2
- package/src/cli.ts +27 -3
- package/src/github-ci/index.test.ts +175 -2
- package/src/github-ci/index.ts +274 -31
- package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
- package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +7 -5
- package/src/monorepo/ci-workflow.ts +16 -6
- package/src/monorepo/managed-files.test.ts +56 -1
- package/src/monorepo/managed-files.ts +20 -1
- package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
- package/src/monorepo/publish-workflow.ts +8 -5
- package/src/monorepo/tool-validation.test.ts +85 -0
- package/src/monorepo/tool-validation.ts +94 -5
- package/src/playwright/index.test.ts +90 -0
- package/src/playwright/index.ts +73 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
- package/src/release/bootstrap-npm-packages.ts +34 -0
- package/src/release/index.ts +30 -4
- package/src/wrangler/cloudflare.test.ts +76 -0
- package/src/wrangler/cloudflare.ts +292 -0
- package/src/wrangler/deploy-environment.test.ts +354 -0
- package/src/wrangler/deploy-environment.ts +445 -0
- package/src/wrangler/environment.test.ts +173 -0
- package/src/wrangler/environment.ts +366 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { run } from '../lib/run.js';
|
|
3
|
+
|
|
4
|
+
const HOST_CACHE_ROOT = '/var/cache/ci';
|
|
5
|
+
const HOST_BROWSER_CACHE = `${HOST_CACHE_ROOT}/ms-playwright`;
|
|
6
|
+
const SYSTEM_CHROME_PATHS = [
|
|
7
|
+
'/usr/bin/google-chrome',
|
|
8
|
+
'/usr/bin/chromium-browser',
|
|
9
|
+
'/usr/bin/chromium',
|
|
10
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export interface ChromiumSetupDependencies {
|
|
14
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
15
|
+
readonly exists: (path: string) => boolean;
|
|
16
|
+
readonly run: (command: string, args: string[], cwd: string, env?: Record<string, string>) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ChromiumSetupResult =
|
|
20
|
+
| { readonly mode: 'persistent-cache'; readonly browserCachePath: string }
|
|
21
|
+
| { readonly mode: 'system'; readonly executablePath: string }
|
|
22
|
+
| { readonly mode: 'developer-cache'; readonly browserCachePath?: string };
|
|
23
|
+
|
|
24
|
+
const defaultDependencies: ChromiumSetupDependencies = {
|
|
25
|
+
env: process.env,
|
|
26
|
+
exists: existsSync,
|
|
27
|
+
run,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ensure Chromium is available without polluting ephemeral GitHub runners.
|
|
32
|
+
* Persistent host runners and developer machines may install into their
|
|
33
|
+
* respective caches; ephemeral GitHub runners must use their image's browser.
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureChromium(
|
|
36
|
+
cwd = process.cwd(),
|
|
37
|
+
dependencies: ChromiumSetupDependencies = defaultDependencies,
|
|
38
|
+
): Promise<ChromiumSetupResult> {
|
|
39
|
+
const { env, exists, run: runCommand } = dependencies;
|
|
40
|
+
const githubActions = env.GITHUB_ACTIONS === 'true';
|
|
41
|
+
|
|
42
|
+
if (githubActions && exists(HOST_CACHE_ROOT)) {
|
|
43
|
+
await runCommand('playwright', ['install', 'chromium', '--only-shell'], cwd, {
|
|
44
|
+
PLAYWRIGHT_BROWSERS_PATH: HOST_BROWSER_CACHE,
|
|
45
|
+
});
|
|
46
|
+
console.log(`Chromium ready in persistent cache: ${HOST_BROWSER_CACHE}`);
|
|
47
|
+
return { mode: 'persistent-cache', browserCachePath: HOST_BROWSER_CACHE };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (githubActions) {
|
|
51
|
+
const candidates = [env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, ...SYSTEM_CHROME_PATHS].filter(
|
|
52
|
+
(path): path is string => typeof path === 'string' && path.length > 0,
|
|
53
|
+
);
|
|
54
|
+
const executablePath = candidates.find(exists);
|
|
55
|
+
if (!executablePath) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`GitHub-hosted runner has no preinstalled Chromium. Refusing to download; searched: ${candidates.join(', ')}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
console.log(`Using preinstalled Chromium: ${executablePath}`);
|
|
61
|
+
return { mode: 'system', executablePath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const browserCachePath = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
65
|
+
await runCommand(
|
|
66
|
+
'playwright',
|
|
67
|
+
['install', 'chromium', '--only-shell'],
|
|
68
|
+
cwd,
|
|
69
|
+
browserCachePath ? { PLAYWRIGHT_BROWSERS_PATH: browserCachePath } : undefined,
|
|
70
|
+
);
|
|
71
|
+
console.log(browserCachePath ? `Chromium ready in configured cache: ${browserCachePath}` : 'Chromium ready.');
|
|
72
|
+
return browserCachePath ? { mode: 'developer-cache', browserCachePath } : { mode: 'developer-cache' };
|
|
73
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
bootstrapNpmPackages,
|
|
5
5
|
NPM_BOOTSTRAP_DIST_TAG,
|
|
6
6
|
NPM_BOOTSTRAP_VERSION,
|
|
7
|
+
NPM_BOOTSTRAP_VISIBILITY_TIMEOUT_MS,
|
|
7
8
|
} from '../bootstrap-npm-packages.js';
|
|
8
9
|
import type { ReleasePackageInfo } from '../core.js';
|
|
9
10
|
|
|
@@ -70,6 +71,45 @@ describe('bootstrap npm packages', () => {
|
|
|
70
71
|
expect(shell.prompts).toEqual([]);
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
it('uploads every placeholder before polling all pending packages with backoff', async () => {
|
|
75
|
+
const shell = new RecordingBootstrapShell({
|
|
76
|
+
packages: [stable, missing],
|
|
77
|
+
existing: [],
|
|
78
|
+
visibleAfterChecks: { [stable.name]: 2, [missing.name]: 3 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await bootstrapNpmPackages(shell, { dryRun: false, skipLogin: true, packages: [], otp: '111222' });
|
|
82
|
+
|
|
83
|
+
expect(shell.events).toEqual([
|
|
84
|
+
`publish:${stable.name}`,
|
|
85
|
+
`publish:${missing.name}`,
|
|
86
|
+
`view:${stable.name}:${NPM_BOOTSTRAP_VERSION}`,
|
|
87
|
+
`view:${missing.name}:${NPM_BOOTSTRAP_VERSION}`,
|
|
88
|
+
'wait:1000',
|
|
89
|
+
`view:${stable.name}:${NPM_BOOTSTRAP_VERSION}`,
|
|
90
|
+
`view:${missing.name}:${NPM_BOOTSTRAP_VERSION}`,
|
|
91
|
+
'wait:2000',
|
|
92
|
+
`view:${missing.name}:${NPM_BOOTSTRAP_VERSION}`,
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('times out after bounded backoff without suggesting another bootstrap', async () => {
|
|
97
|
+
const shell = new RecordingBootstrapShell({
|
|
98
|
+
packages: [missing],
|
|
99
|
+
existing: [],
|
|
100
|
+
visibleAfterChecks: { [missing.name]: Number.POSITIVE_INFINITY },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
bootstrapNpmPackages(shell, { dryRun: false, skipLogin: true, packages: [], otp: '111222' }),
|
|
105
|
+
).rejects.toThrow('Retry smoo release trust-publisher later; do not bootstrap again.');
|
|
106
|
+
|
|
107
|
+
const waited = shell.events
|
|
108
|
+
.filter((event) => event.startsWith('wait:'))
|
|
109
|
+
.reduce((total, event) => total + Number(event.slice('wait:'.length)), 0);
|
|
110
|
+
expect(waited).toBe(NPM_BOOTSTRAP_VISIBILITY_TIMEOUT_MS);
|
|
111
|
+
});
|
|
112
|
+
|
|
73
113
|
it('rejects unknown package selections before npm login', async () => {
|
|
74
114
|
const shell = new RecordingBootstrapShell({ packages: [stable], existing: [] });
|
|
75
115
|
|
|
@@ -82,6 +122,7 @@ describe('bootstrap npm packages', () => {
|
|
|
82
122
|
});
|
|
83
123
|
|
|
84
124
|
class RecordingBootstrapShell implements BootstrapNpmPackagesShell<ReleasePackageInfo> {
|
|
125
|
+
readonly events: string[] = [];
|
|
85
126
|
readonly logs: string[] = [];
|
|
86
127
|
readonly published: Array<{ name: string; otp: string }> = [];
|
|
87
128
|
readonly prompts: string[] = [];
|
|
@@ -89,11 +130,19 @@ class RecordingBootstrapShell implements BootstrapNpmPackagesShell<ReleasePackag
|
|
|
89
130
|
private readonly packages: ReleasePackageInfo[];
|
|
90
131
|
private readonly existing: Set<string>;
|
|
91
132
|
private readonly otps: string[];
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
private readonly visibilityChecks = new Map<string, number>();
|
|
134
|
+
private readonly visibleAfterChecks: Readonly<Record<string, number>>;
|
|
135
|
+
|
|
136
|
+
constructor(options: {
|
|
137
|
+
packages: ReleasePackageInfo[];
|
|
138
|
+
existing: string[];
|
|
139
|
+
otps?: string[];
|
|
140
|
+
visibleAfterChecks?: Record<string, number>;
|
|
141
|
+
}) {
|
|
94
142
|
this.packages = options.packages;
|
|
95
143
|
this.existing = new Set(options.existing);
|
|
96
144
|
this.otps = [...(options.otps ?? [])];
|
|
145
|
+
this.visibleAfterChecks = options.visibleAfterChecks ?? {};
|
|
97
146
|
}
|
|
98
147
|
|
|
99
148
|
listReleasePackages(): ReleasePackageInfo[] {
|
|
@@ -104,11 +153,19 @@ class RecordingBootstrapShell implements BootstrapNpmPackagesShell<ReleasePackag
|
|
|
104
153
|
return this.existing.has(name);
|
|
105
154
|
}
|
|
106
155
|
|
|
156
|
+
async packageVersionExists(name: string, version: string): Promise<boolean> {
|
|
157
|
+
this.events.push(`view:${name}:${version}`);
|
|
158
|
+
const checks = (this.visibilityChecks.get(name) ?? 0) + 1;
|
|
159
|
+
this.visibilityChecks.set(name, checks);
|
|
160
|
+
return checks >= (this.visibleAfterChecks[name] ?? 1);
|
|
161
|
+
}
|
|
162
|
+
|
|
107
163
|
async login(): Promise<void> {
|
|
108
164
|
this.logins += 1;
|
|
109
165
|
}
|
|
110
166
|
|
|
111
167
|
async publishPlaceholder(pkg: ReleasePackageInfo, env?: Record<string, string>): Promise<void> {
|
|
168
|
+
this.events.push(`publish:${pkg.name}`);
|
|
112
169
|
this.published.push({ name: pkg.name, otp: env?.NPM_CONFIG_OTP ?? '' });
|
|
113
170
|
}
|
|
114
171
|
|
|
@@ -121,6 +178,10 @@ class RecordingBootstrapShell implements BootstrapNpmPackagesShell<ReleasePackag
|
|
|
121
178
|
return otp;
|
|
122
179
|
}
|
|
123
180
|
|
|
181
|
+
async wait(milliseconds: number): Promise<void> {
|
|
182
|
+
this.events.push(`wait:${milliseconds}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
124
185
|
log(message: string): void {
|
|
125
186
|
this.logs.push(message);
|
|
126
187
|
}
|
|
@@ -2,6 +2,9 @@ import type { ReleasePackageInfo } from './core.js';
|
|
|
2
2
|
|
|
3
3
|
export const NPM_BOOTSTRAP_VERSION = '0.0.0-bootstrap.0';
|
|
4
4
|
export const NPM_BOOTSTRAP_DIST_TAG = 'bootstrap';
|
|
5
|
+
export const NPM_BOOTSTRAP_VISIBILITY_TIMEOUT_MS = 120_000;
|
|
6
|
+
|
|
7
|
+
const NPM_BOOTSTRAP_POLL_DELAYS_MS = [0, 1_000, 2_000, 4_000, 8_000, 15_000, 30_000, 30_000, 30_000] as const;
|
|
5
8
|
|
|
6
9
|
export interface BootstrapNpmPackagesOptions {
|
|
7
10
|
dryRun: boolean;
|
|
@@ -13,9 +16,11 @@ export interface BootstrapNpmPackagesOptions {
|
|
|
13
16
|
export interface BootstrapNpmPackagesShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
14
17
|
listReleasePackages(): Package[];
|
|
15
18
|
packageExists(name: string): Promise<boolean>;
|
|
19
|
+
packageVersionExists(name: string, version: string): Promise<boolean>;
|
|
16
20
|
login(): Promise<void>;
|
|
17
21
|
publishPlaceholder(pkg: Package, env?: Record<string, string>): Promise<void>;
|
|
18
22
|
promptOtp(packageName: string): Promise<string>;
|
|
23
|
+
wait(milliseconds: number): Promise<void>;
|
|
19
24
|
log(message: string): void;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -54,15 +59,44 @@ export async function bootstrapNpmPackages<Package extends ReleasePackageInfo>(
|
|
|
54
59
|
if (!options.skipLogin) {
|
|
55
60
|
await shell.login();
|
|
56
61
|
}
|
|
62
|
+
// Upload every placeholder before waiting for npm registry propagation. Keeping the
|
|
63
|
+
// phases separate prevents one slow package from blocking the remaining uploads.
|
|
57
64
|
for (const pkg of missing) {
|
|
58
65
|
shell.log(`${pkg.name}: publishing npm placeholder.`);
|
|
59
66
|
const otp = options.otp ?? (await shell.promptOtp(pkg.name));
|
|
60
67
|
await shell.publishPlaceholder(pkg, { NPM_CONFIG_OTP: otp });
|
|
61
68
|
}
|
|
69
|
+
|
|
70
|
+
await waitForPublishedPackages(shell, missing);
|
|
62
71
|
shell.log('Bootstrap complete. Run smoo release trust-publisher before the first CI publish.');
|
|
63
72
|
return missing;
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
async function waitForPublishedPackages<Package extends ReleasePackageInfo>(
|
|
76
|
+
shell: BootstrapNpmPackagesShell<Package>,
|
|
77
|
+
packages: readonly Package[],
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
let pending = [...packages];
|
|
80
|
+
for (const delayMs of NPM_BOOTSTRAP_POLL_DELAYS_MS) {
|
|
81
|
+
if (delayMs > 0) {
|
|
82
|
+
await shell.wait(delayMs);
|
|
83
|
+
}
|
|
84
|
+
const visible = await Promise.all(
|
|
85
|
+
pending.map((pkg) => shell.packageVersionExists(pkg.name, NPM_BOOTSTRAP_VERSION)),
|
|
86
|
+
);
|
|
87
|
+
pending = pending.filter((_, index) => !visible[index]);
|
|
88
|
+
if (pending.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Bootstrap uploads succeeded, but npm did not expose these packages after ${NPM_BOOTSTRAP_VISIBILITY_TIMEOUT_MS / 1_000} seconds: ${pending
|
|
95
|
+
.map((pkg) => pkg.name)
|
|
96
|
+
.join(', ')}. Retry smoo release trust-publisher later; do not bootstrap again.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
function selectedReleasePackages<Package extends ReleasePackageInfo>(
|
|
67
101
|
packages: Package[],
|
|
68
102
|
selections: string[],
|
package/src/release/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { Writable } from 'node:stream';
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
8
|
import { $ } from 'bun';
|
|
8
9
|
import typia from 'typia';
|
|
9
10
|
import { githubCiApplyOutputs, githubCiNxRunMany } from '../github-ci/index.js';
|
|
@@ -79,6 +80,7 @@ export interface ReleaseRepairPendingOptions {
|
|
|
79
80
|
|
|
80
81
|
export interface ReleasePlatformOutputsOptions {
|
|
81
82
|
bump: string;
|
|
83
|
+
githubOutput?: string;
|
|
82
84
|
output: string;
|
|
83
85
|
ref?: string;
|
|
84
86
|
targets: string;
|
|
@@ -220,7 +222,7 @@ export async function releaseCollectPlatformOutputs(
|
|
|
220
222
|
: `Release platform outputs: building current outputs for ${packageSummary(currentPackages)}.`,
|
|
221
223
|
);
|
|
222
224
|
const outputRoot = resolve(root, options.output);
|
|
223
|
-
await githubCiNxRunMany(root, {
|
|
225
|
+
const currentRuns = await githubCiNxRunMany(root, {
|
|
224
226
|
targets: options.targets,
|
|
225
227
|
projects: releasePackageProjects(currentPackages),
|
|
226
228
|
collectOutputs: join(outputRoot, 'current'),
|
|
@@ -235,6 +237,12 @@ export async function releaseCollectPlatformOutputs(
|
|
|
235
237
|
targets,
|
|
236
238
|
restoreRef,
|
|
237
239
|
);
|
|
240
|
+
if (options.githubOutput) {
|
|
241
|
+
const projects = [
|
|
242
|
+
...new Set(currentRuns.runs.flatMap((run) => run.projects.map((project) => project.project))),
|
|
243
|
+
].sort((left, right) => left.localeCompare(right));
|
|
244
|
+
await appendFile(options.githubOutput, `projects=${projects.join(',')}\n`);
|
|
245
|
+
}
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
@@ -251,9 +259,11 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
|
|
|
251
259
|
{
|
|
252
260
|
listReleasePackages: () => listReleasePackages(root),
|
|
253
261
|
packageExists: (name) => npmPackageExists(root, name),
|
|
262
|
+
packageVersionExists: (name, version) => npmPublishedVersionExists(root, name, version),
|
|
254
263
|
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
255
264
|
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
256
265
|
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
266
|
+
wait: delay,
|
|
257
267
|
log: (message) => console.log(message),
|
|
258
268
|
},
|
|
259
269
|
bootstrapOptions,
|
|
@@ -460,9 +470,11 @@ export async function releaseBootstrapNpmPackages(
|
|
|
460
470
|
{
|
|
461
471
|
listReleasePackages: () => listReleasePackages(root),
|
|
462
472
|
packageExists: (name) => npmPackageExists(root, name),
|
|
473
|
+
packageVersionExists: (name, version) => npmPublishedVersionExists(root, name, version),
|
|
463
474
|
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
464
475
|
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
465
476
|
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
477
|
+
wait: delay,
|
|
466
478
|
log: (message) => console.log(message),
|
|
467
479
|
},
|
|
468
480
|
{
|
|
@@ -961,12 +973,13 @@ function releaseRepairOutputsShell(
|
|
|
961
973
|
): ReleaseRepairOutputsShell<ReleasePackage> {
|
|
962
974
|
return {
|
|
963
975
|
...releaseTargetCheckoutShell(root),
|
|
964
|
-
collectRepairTargetOutputs: (target) =>
|
|
965
|
-
githubCiNxRunMany(root, {
|
|
976
|
+
collectRepairTargetOutputs: async (target) => {
|
|
977
|
+
await githubCiNxRunMany(root, {
|
|
966
978
|
targets: targetGlobs,
|
|
967
979
|
projects: releasePackageProjects(target.npmPackages),
|
|
968
980
|
collectOutputs: join(outputRoot, target.sha),
|
|
969
|
-
})
|
|
981
|
+
});
|
|
982
|
+
},
|
|
970
983
|
};
|
|
971
984
|
}
|
|
972
985
|
|
|
@@ -1506,6 +1519,19 @@ async function npmPackageExists(root: string, name: string): Promise<boolean> {
|
|
|
1506
1519
|
return result.exitCode === 0;
|
|
1507
1520
|
}
|
|
1508
1521
|
|
|
1522
|
+
async function npmPublishedVersionExists(root: string, name: string, version: string): Promise<boolean> {
|
|
1523
|
+
const spec = `${name}@${version}`;
|
|
1524
|
+
const args = ['view', spec, 'version', '--json'];
|
|
1525
|
+
const result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1526
|
+
if (result.exitCode === 0) {
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
if (/\bE404\b|404 Not Found/i.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
throw new Error(npmCommandFailedMessage(args, result.exitCode, result.stdout, result.stderr));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1509
1535
|
async function assertCleanGitTree(root: string): Promise<void> {
|
|
1510
1536
|
const result = await $`git status --porcelain --untracked-files=no`.cwd(root).quiet().nothrow();
|
|
1511
1537
|
if (result.exitCode !== 0) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CloudflareApiError, CloudflareRestClient } from './cloudflare.js';
|
|
3
|
+
|
|
4
|
+
type CloudflareFetcher = NonNullable<ConstructorParameters<typeof CloudflareRestClient>[2]>;
|
|
5
|
+
|
|
6
|
+
function jsonFetcher(body: unknown, status = 200): CloudflareFetcher {
|
|
7
|
+
return async () =>
|
|
8
|
+
new Response(JSON.stringify(body), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('CloudflareRestClient', () => {
|
|
15
|
+
it('accepts nullable diagnostics in successful API envelopes', async () => {
|
|
16
|
+
const client = new CloudflareRestClient(
|
|
17
|
+
'account-id',
|
|
18
|
+
'api-token',
|
|
19
|
+
jsonFetcher({
|
|
20
|
+
result: [
|
|
21
|
+
{
|
|
22
|
+
id: 'domain-id',
|
|
23
|
+
hostname: 'app.pr45.example.com',
|
|
24
|
+
service: 'app-pr45',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
success: true,
|
|
28
|
+
errors: null,
|
|
29
|
+
messages: null,
|
|
30
|
+
result_info: {
|
|
31
|
+
page: 1,
|
|
32
|
+
per_page: 1000,
|
|
33
|
+
count: 1,
|
|
34
|
+
total_count: 1,
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await expect(client.listWorkerDomains()).resolves.toEqual([
|
|
40
|
+
{
|
|
41
|
+
id: 'domain-id',
|
|
42
|
+
hostname: 'app.pr45.example.com',
|
|
43
|
+
service: 'app-pr45',
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('preserves structured Cloudflare API failures', async () => {
|
|
49
|
+
const client = new CloudflareRestClient(
|
|
50
|
+
'account-id',
|
|
51
|
+
'api-token',
|
|
52
|
+
jsonFetcher(
|
|
53
|
+
{
|
|
54
|
+
result: null,
|
|
55
|
+
success: false,
|
|
56
|
+
errors: [{ code: 10000, message: 'Authentication error' }],
|
|
57
|
+
messages: [],
|
|
58
|
+
},
|
|
59
|
+
403,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await client.listWorkerDomains();
|
|
65
|
+
throw new Error('expected listWorkerDomains to fail');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
expect(error).toBeInstanceOf(CloudflareApiError);
|
|
68
|
+
expect(error).toMatchObject({
|
|
69
|
+
message:
|
|
70
|
+
'Cloudflare API /accounts/account-id/workers/domains?per_page=1000&page=1 failed: Authentication error',
|
|
71
|
+
status: 403,
|
|
72
|
+
codes: [10000],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|