@smoothbricks/cli 0.10.8 → 0.10.10
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.js +7 -5
- package/dist/github-ci/index.d.ts +9 -7
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +36 -35
- package/dist/monorepo/ci-workflow.d.ts +3 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +59 -15
- package/dist/monorepo/managed-files.d.ts +1 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +15 -5
- package/dist/monorepo/publish-workflow.d.ts +2 -2
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +7 -7
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +14 -6
- package/dist/wrangler/cloudflare.d.ts +4 -2
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +6 -3
- package/dist/wrangler/{deploy-environment.d.ts → deploy-stage.d.ts} +6 -6
- package/dist/wrangler/deploy-stage.d.ts.map +1 -0
- package/dist/wrangler/{deploy-environment.js → deploy-stage.js} +26 -26
- package/dist/wrangler/stage.d.ts +58 -0
- package/dist/wrangler/stage.d.ts.map +1 -0
- package/dist/wrangler/{environment.js → stage.js} +44 -44
- package/package.json +8 -8
- package/src/cli.ts +11 -8
- package/src/github-ci/index.test.ts +105 -34
- package/src/github-ci/index.ts +53 -46
- package/src/monorepo/__tests__/ci-workflow.test.ts +87 -49
- package/src/monorepo/__tests__/publish-workflow.test.ts +14 -12
- package/src/monorepo/ci-workflow.ts +67 -14
- package/src/monorepo/managed-files.test.ts +26 -5
- package/src/monorepo/managed-files.ts +18 -5
- package/src/monorepo/publish-workflow.ts +15 -12
- package/src/release/index.ts +12 -4
- package/src/wrangler/cloudflare.test.ts +76 -0
- package/src/wrangler/cloudflare.ts +6 -3
- package/src/wrangler/{deploy-environment.test.ts → deploy-stage.test.ts} +11 -11
- package/src/wrangler/{deploy-environment.ts → deploy-stage.ts} +41 -41
- package/src/wrangler/{environment.test.ts → stage.test.ts} +15 -15
- package/src/wrangler/{environment.ts → stage.ts} +49 -52
- package/dist/wrangler/deploy-environment.d.ts.map +0 -1
- package/dist/wrangler/environment.d.ts +0 -58
- package/dist/wrangler/environment.d.ts.map +0 -1
|
@@ -3,40 +3,40 @@ import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
|
3
3
|
import typia from 'typia';
|
|
4
4
|
import { cloneEnvBlock } from './prepare-env.js';
|
|
5
5
|
const MAX_PULL_REQUEST_NUMBER = 999999999;
|
|
6
|
-
const
|
|
7
|
-
export function
|
|
6
|
+
const STAGE_PATTERN = /^(?:staging|production|pr[1-9][0-9]{0,8})$/;
|
|
7
|
+
export function pullRequestStage(prNumber) {
|
|
8
8
|
if (!Number.isInteger(prNumber) || prNumber < 1 || prNumber > MAX_PULL_REQUEST_NUMBER) {
|
|
9
9
|
throw new Error(`Pull request number must be an integer from 1 through ${MAX_PULL_REQUEST_NUMBER}.`);
|
|
10
10
|
}
|
|
11
11
|
return `pr${prNumber}`;
|
|
12
12
|
}
|
|
13
|
-
export function
|
|
14
|
-
if (!
|
|
15
|
-
throw new Error('
|
|
13
|
+
export function parseDeploymentStage(value) {
|
|
14
|
+
if (!STAGE_PATTERN.test(value)) {
|
|
15
|
+
throw new Error('Deployment stage must be exactly staging, production, or pr followed by an integer from 1 through 999999999.');
|
|
16
16
|
}
|
|
17
17
|
if (value === 'staging' || value === 'production')
|
|
18
18
|
return value;
|
|
19
|
-
return
|
|
19
|
+
return pullRequestStage(Number(value.slice(2)));
|
|
20
20
|
}
|
|
21
|
-
export function
|
|
22
|
-
return
|
|
21
|
+
export function isPullRequestStage(stage) {
|
|
22
|
+
return stage.startsWith('pr');
|
|
23
23
|
}
|
|
24
|
-
export function
|
|
25
|
-
const token =
|
|
24
|
+
export function stageDomain(stage, zone) {
|
|
25
|
+
const token = parseDeploymentStage(stage);
|
|
26
26
|
if (!zone || zone.startsWith('.') || zone.endsWith('.')) {
|
|
27
27
|
throw new Error('Zone must be a non-empty DNS name without leading or trailing dots.');
|
|
28
28
|
}
|
|
29
29
|
return token === 'production' ? zone : `${token}.${zone}`;
|
|
30
30
|
}
|
|
31
|
-
export function
|
|
32
|
-
const token =
|
|
31
|
+
export function stageResourceName(base, stage) {
|
|
32
|
+
const token = parseDeploymentStage(stage);
|
|
33
33
|
if (!base) {
|
|
34
34
|
throw new Error('Resource base name must not be empty.');
|
|
35
35
|
}
|
|
36
36
|
return token === 'production' ? base : `${base}-${token}`;
|
|
37
37
|
}
|
|
38
|
-
export function
|
|
39
|
-
const escaped =
|
|
38
|
+
export function hasExactStageSegment(value, stage) {
|
|
39
|
+
const escaped = stage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
40
|
return new RegExp(`(?:^|[-.])${escaped}(?=$|[-.])`).test(value);
|
|
41
41
|
}
|
|
42
42
|
const isWranglerRoot = (() => {
|
|
@@ -86,14 +86,14 @@ const isUnknownRows = (() => {
|
|
|
86
86
|
});
|
|
87
87
|
return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io0(elem));
|
|
88
88
|
})();
|
|
89
|
-
export function
|
|
90
|
-
const block = parseRoot(toml).env?.[
|
|
89
|
+
export function planConfiguredStageResources(toml, stage) {
|
|
90
|
+
const block = parseRoot(toml).env?.[stage];
|
|
91
91
|
if (!isWranglerEnvironment(block)) {
|
|
92
|
-
throw new Error(`Wrangler configuration must declare [env.${
|
|
92
|
+
throw new Error(`Wrangler configuration must declare [env.${stage}].`);
|
|
93
93
|
}
|
|
94
|
-
const workerName = requiredString(block, 'name', `[env.${
|
|
94
|
+
const workerName = requiredString(block, 'name', `[env.${stage}]`);
|
|
95
95
|
return {
|
|
96
|
-
|
|
96
|
+
stage,
|
|
97
97
|
workerName,
|
|
98
98
|
kvNamespaces: readKvBindings(block.kv_namespaces),
|
|
99
99
|
r2Buckets: readRows(block.r2_buckets).map((row) => {
|
|
@@ -127,8 +127,8 @@ function stagingWorkerName(staging) {
|
|
|
127
127
|
}
|
|
128
128
|
return { workerName: staging.name, workerBaseName: staging.name.slice(0, -'-staging'.length) };
|
|
129
129
|
}
|
|
130
|
-
export function planPullRequestResources(toml,
|
|
131
|
-
|
|
130
|
+
export function planPullRequestResources(toml, stage, liveNamespaces) {
|
|
131
|
+
parseDeploymentStage(stage);
|
|
132
132
|
const staging = stagingEnvironment(toml);
|
|
133
133
|
const { workerBaseName } = stagingWorkerName(staging);
|
|
134
134
|
const namespaceById = new Map(liveNamespaces.map((namespace) => [namespace.id, namespace]));
|
|
@@ -137,7 +137,7 @@ export function planPullRequestResources(toml, environment, liveNamespaces) {
|
|
|
137
137
|
if (!stagingNamespace) {
|
|
138
138
|
throw new Error(`Staging KV binding ${binding} references namespace ${id}, which is absent from the account listing.`);
|
|
139
139
|
}
|
|
140
|
-
const title = replaceExactToken(stagingNamespace.title, 'staging',
|
|
140
|
+
const title = replaceExactToken(stagingNamespace.title, 'staging', stage);
|
|
141
141
|
if (title === stagingNamespace.title) {
|
|
142
142
|
throw new Error(`Staging KV namespace title ${stagingNamespace.title} has no exact staging segment.`);
|
|
143
143
|
}
|
|
@@ -146,20 +146,20 @@ export function planPullRequestResources(toml, environment, liveNamespaces) {
|
|
|
146
146
|
const r2Buckets = readRows(staging.r2_buckets).map((row) => {
|
|
147
147
|
const binding = requiredString(row, 'binding', 'R2 binding');
|
|
148
148
|
const stagingBucket = requiredString(row, 'bucket_name', `R2 binding ${binding}`);
|
|
149
|
-
const bucketName = replaceExactToken(stagingBucket, 'staging',
|
|
149
|
+
const bucketName = replaceExactToken(stagingBucket, 'staging', stage);
|
|
150
150
|
if (bucketName === stagingBucket) {
|
|
151
151
|
throw new Error(`Staging R2 bucket ${stagingBucket} has no exact staging segment.`);
|
|
152
152
|
}
|
|
153
153
|
return { binding, bucketName };
|
|
154
154
|
});
|
|
155
155
|
const routes = readRows(staging.routes).map((row) => ({
|
|
156
|
-
pattern: replaceHostnameLabel(requiredString(row, 'pattern', 'route'),
|
|
156
|
+
pattern: replaceHostnameLabel(requiredString(row, 'pattern', 'route'), stage),
|
|
157
157
|
...(typeof row.zone_name === 'string' ? { zoneName: row.zone_name } : {}),
|
|
158
158
|
customDomain: row.custom_domain === true,
|
|
159
159
|
}));
|
|
160
160
|
return {
|
|
161
|
-
|
|
162
|
-
workerName:
|
|
161
|
+
stage,
|
|
162
|
+
workerName: stageResourceName(workerBaseName, stage),
|
|
163
163
|
workerBaseName,
|
|
164
164
|
kvNamespaces,
|
|
165
165
|
r2Buckets,
|
|
@@ -167,14 +167,14 @@ export function planPullRequestResources(toml, environment, liveNamespaces) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
export function derivePullRequestWranglerConfig(toml, options) {
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const stage = options.stage;
|
|
171
|
+
parseDeploymentStage(stage);
|
|
172
172
|
if (!options.accountId) {
|
|
173
173
|
throw new Error('Cloudflare account id is required to derive rate-limit namespaces.');
|
|
174
174
|
}
|
|
175
175
|
const staging = stagingEnvironment(toml);
|
|
176
176
|
const { workerBaseName } = stagingWorkerName(staging);
|
|
177
|
-
const cloned = cloneEnvBlock(toml, 'staging',
|
|
177
|
+
const cloned = cloneEnvBlock(toml, 'staging', stage);
|
|
178
178
|
const program = parseTOML(cloned);
|
|
179
179
|
const rootValue = getStaticTOMLValue(program);
|
|
180
180
|
if (!isWranglerRoot(rootValue)) {
|
|
@@ -183,7 +183,7 @@ export function derivePullRequestWranglerConfig(toml, options) {
|
|
|
183
183
|
const root = rootValue;
|
|
184
184
|
const edits = [];
|
|
185
185
|
for (const table of program.body[0].body) {
|
|
186
|
-
if (table.type !== 'TOMLTable' || table.resolvedKey[0] !== 'env' || table.resolvedKey[1] !==
|
|
186
|
+
if (table.type !== 'TOMLTable' || table.resolvedKey[0] !== 'env' || table.resolvedKey[1] !== stage) {
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
189
|
const tableValue = valueAtPath(root, table.resolvedKey);
|
|
@@ -193,7 +193,7 @@ export function derivePullRequestWranglerConfig(toml, options) {
|
|
|
193
193
|
for (const keyValue of table.body) {
|
|
194
194
|
const key = cloned.slice(keyValue.key.range[0], keyValue.key.range[1]).trim();
|
|
195
195
|
const current = tableValue[key];
|
|
196
|
-
const next = deriveFieldValue(table.resolvedKey.slice(2), tableValue, key, current,
|
|
196
|
+
const next = deriveFieldValue(table.resolvedKey.slice(2), tableValue, key, current, stage, workerBaseName, options.accountId, options.kvNamespaceIds);
|
|
197
197
|
if (next !== current) {
|
|
198
198
|
edits.push({ start: keyValue.value.range[0], end: keyValue.value.range[1], value: tomlLiteral(next) });
|
|
199
199
|
}
|
|
@@ -206,25 +206,25 @@ export function derivePullRequestWranglerConfig(toml, options) {
|
|
|
206
206
|
parseTOML(derived);
|
|
207
207
|
return derived;
|
|
208
208
|
}
|
|
209
|
-
function deriveFieldValue(path, table, key, current,
|
|
209
|
+
function deriveFieldValue(path, table, key, current, stage, workerBaseName, accountId, kvNamespaceIds) {
|
|
210
210
|
const section = path[0];
|
|
211
211
|
if (path.length === 0 && key === 'name') {
|
|
212
|
-
return
|
|
212
|
+
return stageResourceName(workerBaseName, stage);
|
|
213
213
|
}
|
|
214
214
|
if (section === 'routes' && key === 'pattern' && typeof current === 'string') {
|
|
215
|
-
return replaceHostnameLabel(current,
|
|
215
|
+
return replaceHostnameLabel(current, stage);
|
|
216
216
|
}
|
|
217
217
|
if (section === 'vars' && typeof current === 'string') {
|
|
218
218
|
if (key === 'ENVIRONMENT') {
|
|
219
|
-
return
|
|
219
|
+
return stage;
|
|
220
220
|
}
|
|
221
221
|
if (key === 'AUTH_KEYS_INSTANCE_NAME') {
|
|
222
|
-
return replaceExactToken(current, 'staging',
|
|
222
|
+
return replaceExactToken(current, 'staging', stage);
|
|
223
223
|
}
|
|
224
|
-
return replaceHostnameLabel(current,
|
|
224
|
+
return replaceHostnameLabel(current, stage);
|
|
225
225
|
}
|
|
226
226
|
if (section === 'send_email' && key === 'allowed_sender_addresses' && Array.isArray(current)) {
|
|
227
|
-
return current.map((value) => (typeof value === 'string' ? replaceHostnameLabel(value,
|
|
227
|
+
return current.map((value) => (typeof value === 'string' ? replaceHostnameLabel(value, stage) : value));
|
|
228
228
|
}
|
|
229
229
|
if (section === 'kv_namespaces' && key === 'id' && typeof current === 'string') {
|
|
230
230
|
const derived = kvNamespaceIds.get(current);
|
|
@@ -234,22 +234,22 @@ function deriveFieldValue(path, table, key, current, environment, workerBaseName
|
|
|
234
234
|
return derived;
|
|
235
235
|
}
|
|
236
236
|
if (section === 'r2_buckets' && key === 'bucket_name' && typeof current === 'string') {
|
|
237
|
-
return replaceExactToken(current, 'staging',
|
|
237
|
+
return replaceExactToken(current, 'staging', stage);
|
|
238
238
|
}
|
|
239
239
|
if (section === 'ratelimits' && key === 'namespace_id') {
|
|
240
240
|
const bindingName = requiredString(table, 'name', 'Rate-limit binding');
|
|
241
|
-
return rateLimitNamespaceId(accountId, workerBaseName,
|
|
241
|
+
return rateLimitNamespaceId(accountId, workerBaseName, stage, bindingName);
|
|
242
242
|
}
|
|
243
243
|
return current;
|
|
244
244
|
}
|
|
245
|
-
export function rateLimitNamespaceId(accountId, workerBaseName,
|
|
246
|
-
const token =
|
|
245
|
+
export function rateLimitNamespaceId(accountId, workerBaseName, stage, bindingName) {
|
|
246
|
+
const token = parseDeploymentStage(stage);
|
|
247
247
|
const digest = createHash('sha256').update(`${accountId}:${workerBaseName}:${token}:${bindingName}`).digest();
|
|
248
248
|
const value = digest.readUInt32BE(0) & 2147483647;
|
|
249
249
|
return String(value === 0 ? 1 : value);
|
|
250
250
|
}
|
|
251
|
-
function replaceHostnameLabel(value,
|
|
252
|
-
return value.replace(/(^|[.@/])staging(?=\.)/g, `$1${
|
|
251
|
+
function replaceHostnameLabel(value, stage) {
|
|
252
|
+
return value.replace(/(^|[.@/])staging(?=\.)/g, `$1${stage}`);
|
|
253
253
|
}
|
|
254
254
|
function replaceExactToken(value, from, to) {
|
|
255
255
|
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"import": "./dist/wrangler/prepare-env.js",
|
|
49
49
|
"default": "./dist/wrangler/prepare-env.js"
|
|
50
50
|
},
|
|
51
|
-
"./wrangler/
|
|
52
|
-
"types": "./dist/wrangler/
|
|
53
|
-
"bun": "./src/wrangler/
|
|
54
|
-
"development": "./src/wrangler/
|
|
55
|
-
"import": "./dist/wrangler/
|
|
56
|
-
"default": "./dist/wrangler/
|
|
51
|
+
"./wrangler/stage": {
|
|
52
|
+
"types": "./dist/wrangler/stage.d.ts",
|
|
53
|
+
"bun": "./src/wrangler/stage.ts",
|
|
54
|
+
"development": "./src/wrangler/stage.ts",
|
|
55
|
+
"import": "./dist/wrangler/stage.js",
|
|
56
|
+
"default": "./dist/wrangler/stage.js"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"sideEffects": [
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@arethetypeswrong/core": "^0.18.2",
|
|
73
73
|
"@prettier/sync": "^0.6.1",
|
|
74
|
-
"@smoothbricks/nx-plugin": "0.3.
|
|
74
|
+
"@smoothbricks/nx-plugin": "0.3.6",
|
|
75
75
|
"@smoothbricks/validation": "0.1.5",
|
|
76
76
|
"commander": "^14.0.3",
|
|
77
77
|
"prettier": "^3.6.1",
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { cliPackageVersion } from './lib/cli-package.js';
|
|
|
4
4
|
import { findRepoRoot } from './lib/run.js';
|
|
5
5
|
import { ensureChromium } from './playwright/index.js';
|
|
6
6
|
import { resolvePrConflicts } from './pr/index.js';
|
|
7
|
-
import { cleanupPullRequest,
|
|
7
|
+
import { cleanupPullRequest, deployStage } from './wrangler/deploy-stage.js';
|
|
8
8
|
import { scaffold } from './wrangler/scaffold.js';
|
|
9
9
|
|
|
10
10
|
export async function runCli(argv = process.argv.slice(2)): Promise<void> {
|
|
@@ -175,7 +175,8 @@ function buildProgram(): Command {
|
|
|
175
175
|
.requiredOption('--targets <targets>', 'comma-separated Nx platform target names or globs')
|
|
176
176
|
.requiredOption('--output <path>', 'output directory for current and repair artifacts')
|
|
177
177
|
.option('--ref <ref>', 'fixed release graph ref to inspect')
|
|
178
|
-
.
|
|
178
|
+
.option('--github-output <path>', 'append selected current platform projects to a GitHub Actions output file')
|
|
179
|
+
.action(async (options: { bump: string; githubOutput?: string; output: string; ref?: string; targets: string }) => {
|
|
179
180
|
// The source self-hosting shim has no Typia transform; release commands import transformed output validators.
|
|
180
181
|
const { releaseCollectPlatformOutputs } = await import('./release/index.js');
|
|
181
182
|
await releaseCollectPlatformOutputs(await findRepoRoot(), options);
|
|
@@ -334,6 +335,7 @@ function buildProgram(): Command {
|
|
|
334
335
|
.option('--step <step>')
|
|
335
336
|
.option('--mode <mode>', 'auto, affected, or run-many', 'auto')
|
|
336
337
|
.option('--configuration <configuration>')
|
|
338
|
+
.option('--stage <stage>')
|
|
337
339
|
.action(
|
|
338
340
|
async (options: {
|
|
339
341
|
target: string;
|
|
@@ -341,6 +343,7 @@ function buildProgram(): Command {
|
|
|
341
343
|
step?: string;
|
|
342
344
|
mode?: 'auto' | 'affected' | 'run-many';
|
|
343
345
|
configuration?: string;
|
|
346
|
+
stage?: string;
|
|
344
347
|
}) => {
|
|
345
348
|
const { githubCiNxSmart } = await import('./github-ci/index.js');
|
|
346
349
|
await githubCiNxSmart(await findRepoRoot(), options);
|
|
@@ -375,14 +378,14 @@ function buildProgram(): Command {
|
|
|
375
378
|
});
|
|
376
379
|
githubCi
|
|
377
380
|
.command('nx-deploy')
|
|
378
|
-
.option('--
|
|
381
|
+
.option('--stage <stage>', 'explicit staging, production, or prN override')
|
|
379
382
|
.option('--mode <mode>', 'auto, affected, or run-many', 'run-many')
|
|
380
383
|
.option('--name <name>')
|
|
381
384
|
.option('--step <step>')
|
|
382
385
|
.option('--verify', 'run build, lint, and test before deploy')
|
|
383
386
|
.action(
|
|
384
387
|
async (options: {
|
|
385
|
-
|
|
388
|
+
stage?: string;
|
|
386
389
|
mode?: 'auto' | 'affected' | 'run-many';
|
|
387
390
|
name?: string;
|
|
388
391
|
step?: string;
|
|
@@ -423,10 +426,10 @@ function buildProgram(): Command {
|
|
|
423
426
|
scaffold(await findRepoRoot(), project, { force: options.force });
|
|
424
427
|
});
|
|
425
428
|
wrangler
|
|
426
|
-
.command('deploy-
|
|
427
|
-
.requiredOption('--
|
|
428
|
-
.action(async (options: {
|
|
429
|
-
await
|
|
429
|
+
.command('deploy-stage')
|
|
430
|
+
.requiredOption('--stage <stage>', 'staging, production, or prN')
|
|
431
|
+
.action(async (options: { stage: string }) => {
|
|
432
|
+
await deployStage(process.cwd(), options.stage);
|
|
430
433
|
});
|
|
431
434
|
wrangler
|
|
432
435
|
.command('cleanup-pr')
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
nxSmartArgs,
|
|
15
15
|
publishGithubDeployment,
|
|
16
16
|
readGitHeadSha,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
resolveDeploymentStage,
|
|
18
|
+
selectStageDeployProjects,
|
|
19
19
|
} from './index.js';
|
|
20
20
|
import {
|
|
21
21
|
applyCollectedOutputs,
|
|
@@ -134,12 +134,13 @@ describe('GitHub CI Nx target expansion', () => {
|
|
|
134
134
|
).toEqual(['native:package-linux', 'native:compile-linux', 'native:build']);
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
it('adds the generic target skip tag only to nx-smart', () => {
|
|
138
|
-
expect(nxSmartArgs('
|
|
139
|
-
'
|
|
137
|
+
it('adds optional stage before the generic target skip tag only to nx-smart', () => {
|
|
138
|
+
expect(nxSmartArgs('e2e-deployment', 'run-many', undefined, 'pr123')).toEqual([
|
|
139
|
+
'run-many',
|
|
140
140
|
'-t',
|
|
141
|
-
'
|
|
142
|
-
'--
|
|
141
|
+
'e2e-deployment',
|
|
142
|
+
'--stage=pr123',
|
|
143
|
+
'--exclude=tag:ci:skip:e2e-deployment',
|
|
143
144
|
'--parallel=100%',
|
|
144
145
|
]);
|
|
145
146
|
expect(nxRunManyArgs({ target: 'test', projects: projects.slice(0, 1) })).not.toContain(
|
|
@@ -166,11 +167,12 @@ describe('collected Nx outputs', () => {
|
|
|
166
167
|
|
|
167
168
|
it('collects an empty artifact when selected projects have no matching target', async () => {
|
|
168
169
|
await withNxRunManyFixture(async ({ root, artifact }) => {
|
|
169
|
-
await githubCiNxRunMany(root, {
|
|
170
|
+
const expanded = await githubCiNxRunMany(root, {
|
|
170
171
|
targets: '*-linux',
|
|
171
172
|
projects: 'app',
|
|
172
173
|
collectOutputs: artifact,
|
|
173
174
|
});
|
|
175
|
+
expect(expanded.runs).toEqual([]);
|
|
174
176
|
|
|
175
177
|
const sourceSha = await readGitHeadSha(root);
|
|
176
178
|
expect(JSON.parse(await readFile(join(artifact, 'manifest.json'), 'utf8'))).toEqual({
|
|
@@ -186,10 +188,11 @@ describe('collected Nx outputs', () => {
|
|
|
186
188
|
await writeFile(join(root, 'packages/app/test-target.ts'), "await Bun.write('test-ran.txt', 'tested');\n");
|
|
187
189
|
await writeFile(join(root, 'packages/app/build-target.ts'), "throw new Error('build must not run here');\n");
|
|
188
190
|
|
|
189
|
-
await githubCiNxRunMany(root, {
|
|
191
|
+
const expanded = await githubCiNxRunMany(root, {
|
|
190
192
|
targets: 'test',
|
|
191
193
|
projectsWithTargets: '*-macos,*-ios',
|
|
192
194
|
});
|
|
195
|
+
expect(expanded.runs.map((run) => run.projects.map((project) => project.project))).toEqual([['app']]);
|
|
193
196
|
|
|
194
197
|
await expect(readFile(join(root, 'test-ran.txt'), 'utf8')).resolves.toBe('tested');
|
|
195
198
|
await expect(readFile(join(root, 'build-ran.txt'), 'utf8')).rejects.toThrow();
|
|
@@ -538,10 +541,10 @@ async function withNxRunManyFixture(
|
|
|
538
541
|
}
|
|
539
542
|
}
|
|
540
543
|
|
|
541
|
-
describe('event-aware
|
|
542
|
-
it('resolves same-repository PR, private push, release, and explicit production
|
|
544
|
+
describe('event-aware stage deployment', () => {
|
|
545
|
+
it('resolves same-repository PR, private push, release, and explicit production stages', () => {
|
|
543
546
|
expect(
|
|
544
|
-
|
|
547
|
+
resolveDeploymentStage(
|
|
545
548
|
undefined,
|
|
546
549
|
{ GITHUB_EVENT_NAME: 'pull_request' },
|
|
547
550
|
{
|
|
@@ -552,12 +555,12 @@ describe('event-aware environment deployment', () => {
|
|
|
552
555
|
),
|
|
553
556
|
).toBe('pr123');
|
|
554
557
|
expect(
|
|
555
|
-
|
|
558
|
+
resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'push', GITHUB_REF_NAME: 'private' }, undefined),
|
|
556
559
|
).toBe('staging');
|
|
557
|
-
expect(
|
|
558
|
-
expect(
|
|
560
|
+
expect(resolveDeploymentStage(undefined, { GITHUB_EVENT_NAME: 'release' }, undefined)).toBe('production');
|
|
561
|
+
expect(resolveDeploymentStage('production', {}, undefined)).toBe('production');
|
|
559
562
|
expect(() =>
|
|
560
|
-
|
|
563
|
+
resolveDeploymentStage(
|
|
561
564
|
undefined,
|
|
562
565
|
{ GITHUB_EVENT_NAME: 'pull_request' },
|
|
563
566
|
{
|
|
@@ -569,15 +572,15 @@ describe('event-aware environment deployment', () => {
|
|
|
569
572
|
).toThrow(/same-repository/);
|
|
570
573
|
});
|
|
571
574
|
|
|
572
|
-
it('selects only deploy targets owned by the
|
|
575
|
+
it('selects only deploy targets owned by the stage convention', async () => {
|
|
573
576
|
const definitions: Record<string, unknown> = {
|
|
574
577
|
'conloca-app': {
|
|
575
578
|
targets: {
|
|
576
|
-
deploy: { options: { command: 'smoo wrangler deploy-
|
|
579
|
+
deploy: { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
|
|
577
580
|
},
|
|
578
581
|
},
|
|
579
582
|
'conloca-app-backend': {
|
|
580
|
-
targets: { deploy: { command: 'smoo wrangler deploy-
|
|
583
|
+
targets: { deploy: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
|
|
581
584
|
},
|
|
582
585
|
'conloca-oauth-redirect': {
|
|
583
586
|
targets: { deploy: { options: { command: 'wrangler deploy --config wrangler.toml' } } },
|
|
@@ -588,7 +591,7 @@ describe('event-aware environment deployment', () => {
|
|
|
588
591
|
};
|
|
589
592
|
|
|
590
593
|
await expect(
|
|
591
|
-
|
|
594
|
+
selectStageDeployProjects(Object.keys(definitions), async (project) => definitions[project]),
|
|
592
595
|
).resolves.toEqual(['conloca-app', 'conloca-app-backend']);
|
|
593
596
|
});
|
|
594
597
|
|
|
@@ -652,19 +655,21 @@ describe('event-aware environment deployment', () => {
|
|
|
652
655
|
]);
|
|
653
656
|
});
|
|
654
657
|
|
|
655
|
-
it('deploys app/backend,
|
|
658
|
+
it('deploys app/backend, publishes PR metadata, and emits the resolved stage', async () => {
|
|
656
659
|
const nxCalls: string[][] = [];
|
|
657
660
|
const listCalls: Array<[string, string]> = [];
|
|
658
661
|
const summaries: string[] = [];
|
|
659
662
|
const deployments: Array<[string, string]> = [];
|
|
663
|
+
const outputs: string[] = [];
|
|
660
664
|
|
|
661
665
|
await githubCiNxDeploy(
|
|
662
666
|
'/repo',
|
|
663
|
-
{ mode: 'run-many', name: 'Deploy
|
|
667
|
+
{ mode: 'run-many', name: 'Deploy Stage' },
|
|
664
668
|
{
|
|
665
669
|
processEnv: {
|
|
666
670
|
GITHUB_EVENT_NAME: 'pull_request',
|
|
667
671
|
GITHUB_STEP_SUMMARY: '/summary',
|
|
672
|
+
GITHUB_OUTPUT: '/output',
|
|
668
673
|
},
|
|
669
674
|
setStatus: async () => {},
|
|
670
675
|
eventPayload: {
|
|
@@ -674,7 +679,7 @@ describe('event-aware environment deployment', () => {
|
|
|
674
679
|
},
|
|
675
680
|
listProjects: async (_root, target, mode) => {
|
|
676
681
|
listCalls.push([target, mode]);
|
|
677
|
-
return
|
|
682
|
+
return ['conloca-app', 'conloca-app-backend'];
|
|
678
683
|
},
|
|
679
684
|
runNx: async (args) => {
|
|
680
685
|
nxCalls.push(args);
|
|
@@ -683,24 +688,90 @@ describe('event-aware environment deployment', () => {
|
|
|
683
688
|
appendSummary: async (_path, content) => {
|
|
684
689
|
summaries.push(content);
|
|
685
690
|
},
|
|
686
|
-
|
|
687
|
-
|
|
691
|
+
appendOutput: async (_path, content) => {
|
|
692
|
+
outputs.push(content);
|
|
693
|
+
},
|
|
694
|
+
publishDeployment: async (stage, url) => {
|
|
695
|
+
deployments.push([stage, url]);
|
|
688
696
|
},
|
|
689
697
|
},
|
|
690
698
|
);
|
|
691
699
|
|
|
692
|
-
expect(listCalls).toEqual([
|
|
693
|
-
|
|
694
|
-
['e2e-deployed', 'run-many'],
|
|
695
|
-
]);
|
|
696
|
-
expect(nxCalls).toHaveLength(2);
|
|
700
|
+
expect(listCalls).toEqual([['deploy', 'run-many']]);
|
|
701
|
+
expect(nxCalls).toHaveLength(1);
|
|
697
702
|
expect(nxCalls[0]).toContain('--projects=conloca-app,conloca-app-backend');
|
|
698
703
|
expect(nxCalls[0]).toContain('--exclude=tag:permanent-deploy-target');
|
|
699
|
-
expect(nxCalls[0]).toContain('--
|
|
700
|
-
expect(nxCalls[
|
|
701
|
-
expect(nxCalls[1]).toContain('e2e-deployed');
|
|
702
|
-
expect(nxCalls[1]).toContain('--environment=pr123');
|
|
704
|
+
expect(nxCalls[0]).toContain('--stage=pr123');
|
|
705
|
+
expect(nxCalls[0]).not.toContain('e2e-deployment');
|
|
703
706
|
expect(summaries).toEqual(['## pr123 deployment\n\n[View deployment](https://app.pr123.conloca.com)\n']);
|
|
704
707
|
expect(deployments).toEqual([['pr123', 'https://app.pr123.conloca.com']]);
|
|
708
|
+
expect(outputs).toEqual(['stage=pr123\n']);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('emits no stage when no deploy project exists', async () => {
|
|
712
|
+
const outputs: string[] = [];
|
|
713
|
+
const statuses: string[] = [];
|
|
714
|
+
|
|
715
|
+
await githubCiNxDeploy(
|
|
716
|
+
'/repo',
|
|
717
|
+
{ stage: 'staging' },
|
|
718
|
+
{
|
|
719
|
+
processEnv: { GITHUB_OUTPUT: '/output' },
|
|
720
|
+
listProjects: async () => [],
|
|
721
|
+
appendOutput: async (_path, content) => {
|
|
722
|
+
outputs.push(content);
|
|
723
|
+
},
|
|
724
|
+
setStatus: async (status) => {
|
|
725
|
+
statuses.push(status);
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
expect(outputs).toEqual([]);
|
|
731
|
+
expect(statuses).toEqual(['pending', 'success']);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('emits no stage after deployment or output failure', async () => {
|
|
735
|
+
const deploymentOutputs: string[] = [];
|
|
736
|
+
const deploymentStatuses: string[] = [];
|
|
737
|
+
await expect(
|
|
738
|
+
githubCiNxDeploy(
|
|
739
|
+
'/repo',
|
|
740
|
+
{ stage: 'staging' },
|
|
741
|
+
{
|
|
742
|
+
processEnv: { GITHUB_OUTPUT: '/output' },
|
|
743
|
+
listProjects: async () => ['app'],
|
|
744
|
+
runNx: async () => 1,
|
|
745
|
+
appendOutput: async (_path, content) => {
|
|
746
|
+
deploymentOutputs.push(content);
|
|
747
|
+
},
|
|
748
|
+
setStatus: async (status) => {
|
|
749
|
+
deploymentStatuses.push(status);
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
),
|
|
753
|
+
).rejects.toThrow(/failed with exit code 1/);
|
|
754
|
+
expect(deploymentOutputs).toEqual([]);
|
|
755
|
+
expect(deploymentStatuses).toEqual(['pending', 'failure']);
|
|
756
|
+
|
|
757
|
+
const outputStatuses: string[] = [];
|
|
758
|
+
await expect(
|
|
759
|
+
githubCiNxDeploy(
|
|
760
|
+
'/repo',
|
|
761
|
+
{ stage: 'staging' },
|
|
762
|
+
{
|
|
763
|
+
processEnv: { GITHUB_OUTPUT: '/output' },
|
|
764
|
+
listProjects: async () => ['app'],
|
|
765
|
+
runNx: async () => 0,
|
|
766
|
+
appendOutput: async () => {
|
|
767
|
+
throw new Error('output unavailable');
|
|
768
|
+
},
|
|
769
|
+
setStatus: async (status) => {
|
|
770
|
+
outputStatuses.push(status);
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
),
|
|
774
|
+
).rejects.toThrow('output unavailable');
|
|
775
|
+
expect(outputStatuses).toEqual(['pending', 'failure']);
|
|
705
776
|
});
|
|
706
777
|
});
|