@smoothbricks/cli 0.11.17 → 0.11.19
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 +45 -10
- package/dist/cli.js +10 -7
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +6 -20
- package/dist/lib/deploy-tags.d.ts +17 -3
- package/dist/lib/deploy-tags.d.ts.map +1 -1
- package/dist/lib/deploy-tags.js +23 -4
- package/dist/monorepo/ci-workflow.d.ts +56 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +199 -2
- package/dist/monorepo/managed-files.d.ts +28 -2
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +74 -22
- package/dist/release/github-release.d.ts +10 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +4 -5
- package/dist/secrets/commands.d.ts +52 -13
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +220 -34
- package/dist/secrets/index.d.ts +21 -1
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +110 -4
- package/dist/secrets/repository.d.ts +60 -0
- package/dist/secrets/repository.d.ts.map +1 -0
- package/dist/secrets/repository.js +145 -0
- package/dist/wrangler/cloudflare.d.ts +7 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +10 -0
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +50 -24
- package/dist/wrangler/stage-secrets.d.ts +57 -0
- package/dist/wrangler/stage-secrets.d.ts.map +1 -0
- package/dist/wrangler/stage-secrets.js +178 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
- package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
- package/package.json +2 -2
- package/src/cli.ts +33 -10
- package/src/github-ci/index.test.ts +6 -7
- package/src/github-ci/index.ts +11 -19
- package/src/lib/deploy-tags.ts +22 -4
- package/src/monorepo/__tests__/ci-workflow.test.ts +161 -0
- package/src/monorepo/ci-workflow.ts +277 -2
- package/src/monorepo/managed-files.test.ts +96 -37
- package/src/monorepo/managed-files.ts +94 -23
- package/src/monorepo/package-policy.test.ts +1 -1
- package/src/release/__tests__/github-release.test.ts +8 -4
- package/src/release/github-release.ts +13 -5
- package/src/release/index.ts +4 -4
- package/src/secrets/commands.test.ts +28 -0
- package/src/secrets/commands.ts +237 -35
- package/src/secrets/index.test.ts +118 -1
- package/src/secrets/index.ts +108 -4
- package/src/secrets/repository.test.ts +98 -0
- package/src/secrets/repository.ts +164 -0
- package/src/wrangler/cloudflare.ts +18 -0
- package/src/wrangler/deploy-stage.test.ts +258 -11
- package/src/wrangler/deploy-stage.ts +70 -38
- package/src/wrangler/stage-secrets.ts +146 -0
package/README.md
CHANGED
|
@@ -351,10 +351,18 @@ wraps prose body paragraphs through `fmt -w 72` while preserving fenced code blo
|
|
|
351
351
|
bullets, trailers, URLs, and comment lines.
|
|
352
352
|
|
|
353
353
|
The generated pre-push hook runs only on macOS. Linux `nx lint` already compiles the Linux `cfg(target_os)` arm; Darwin
|
|
354
|
-
does not. The hook runs `nx run-many -t cargo-lint-cross
|
|
355
|
-
prior real Linux clippy and
|
|
356
|
-
`
|
|
357
|
-
|
|
354
|
+
does not. The hook is a probe: it runs `env -u CC_x86_64_unknown_linux_gnu nx run-many -t cargo-lint-cross` and nothing
|
|
355
|
+
else. Nx caches that target on the Cargo inputs, so a hit is a prior real Linux clippy and the push proceeds. A miss
|
|
356
|
+
refuses the push and names `bun run check:linux` — the hook never enters linux-cross and never starts a compile, so what
|
|
357
|
+
it costs is one Nx graph construction rather than an unbounded clippy. `bun run check:linux` is
|
|
358
|
+
`tooling/devenv -P linux-cross shell --` around that same Nx target; it is the only place the gate actually runs, so it
|
|
359
|
+
is deliberately not quiet. `git push --no-verify` skips the hook.
|
|
360
|
+
|
|
361
|
+
The toolchain variable is unset for the probe on purpose. The target's own command reads it to decide whether the cross
|
|
362
|
+
toolchain is present, so a push made from inside an already-entered linux-cross shell would otherwise fall through into
|
|
363
|
+
a real multi-minute compile — the thing the probe exists to avoid. It is not a declared input of the target, so
|
|
364
|
+
unsetting it cannot change the task hash: a warm entry still hits. Measured both ways, including with the variable set
|
|
365
|
+
to a nonexistent compiler.
|
|
358
366
|
|
|
359
367
|
Conventional commit scopes should use Nx project names. For packages in the same npm scope as the root package, smoo
|
|
360
368
|
requires `package.json` `nx.name` to be the unscoped package name, such as `cli` for `@smoothbricks/cli`, so subjects
|
|
@@ -422,11 +430,28 @@ access makes cleanup refuse before it deletes anything rather than half-clean a
|
|
|
422
430
|
A wrong value type anywhere in `smoo.github` fails `smoo monorepo update` and `smoo github-ci nx-deploy` with the
|
|
423
431
|
offending path, rather than silently falling back to the defaults.
|
|
424
432
|
|
|
433
|
+
#### What CI deploys: the deploy tags
|
|
434
|
+
|
|
435
|
+
A `deploy` target says a project CAN be deployed. Four Nx tags say whether CI does, and they are the only thing that
|
|
436
|
+
says so: the generated workflows and `smoo github-ci nx-deploy` read one rule, so a repository cannot render a deploy
|
|
437
|
+
job that deploys nothing, nor deploy a project no job announced.
|
|
438
|
+
|
|
439
|
+
- `stage-deploy-target`: deployed on every stage — pull-request previews, staging, and production.
|
|
440
|
+
- `staging-deploy-target`: deployed on staging only, for infrastructure the pull-request stages share.
|
|
441
|
+
- `permanent-deploy-target`: excluded from every stage deploy; deployed outside the stage flow.
|
|
442
|
+
- `production-push-deploy-target`: also deployed by the generated production-on-push job (below).
|
|
443
|
+
|
|
444
|
+
An untagged `deploy` target is deployable by hand — `nx run <project>:deploy --stage=pr42` — and invisible to CI. A
|
|
445
|
+
repository where no project carries one of these tags gets no deploy step, no `deployments: write` permission, no
|
|
446
|
+
Cloudflare credentials and no `pr-preview-cleanup.yml`, however many wrangler manifests its packages hold: a published
|
|
447
|
+
library ships one to document a Durable Object binding for its consumers, and that manifest looks exactly like a
|
|
448
|
+
deployable worker's.
|
|
449
|
+
|
|
425
450
|
Tag a project `production-push-deploy-target` to have it deployed to production by a generated `deploy-production` job
|
|
426
451
|
that runs after Validate and the e2e job succeed on a push to the staging push branch
|
|
427
|
-
(`smoo github-ci nx-deploy --stage production --select-tag production-push-deploy-target`). The project must also
|
|
428
|
-
|
|
429
|
-
|
|
452
|
+
(`smoo github-ci nx-deploy --stage production --select-tag production-push-deploy-target`). The project must also carry
|
|
453
|
+
`stage-deploy-target`, otherwise `--select-tag` finds nothing and the job logs
|
|
454
|
+
`No run-many deploy projects; skipping production.`
|
|
430
455
|
|
|
431
456
|
#### Ordering one deploy after another
|
|
432
457
|
|
|
@@ -444,9 +469,9 @@ drop.
|
|
|
444
469
|
|
|
445
470
|
What makes the edge safe is that a deploy is never cached and always cheap when there is nothing to do:
|
|
446
471
|
|
|
447
|
-
- `@smoothbricks/nx-plugin` gives
|
|
448
|
-
|
|
449
|
-
|
|
472
|
+
- `@smoothbricks/nx-plugin` gives the `deploy` it infers `cache: false`. An Nx cache hit on a deploy means "we once
|
|
473
|
+
uploaded this hash", which a rollback silently falsifies — the workspace is unchanged, so the hash is unchanged, so a
|
|
474
|
+
cached deploy would report success while the previous version keeps serving.
|
|
450
475
|
- `smoo wrangler deploy-stage` reads live state first. If the version tagged with this task's hash is already the one
|
|
451
476
|
serving 100% of traffic, it returns `remote-cache-hit` after two API calls: no upload, no migration, no traffic shift.
|
|
452
477
|
- The expensive, purely file-derived half belongs in a sibling `deploy-build` target (build the artifact, register it,
|
|
@@ -718,6 +743,16 @@ resource carrying the `prN` segment, D1 included.
|
|
|
718
743
|
fall back to that variable and rename the worker after it.
|
|
719
744
|
- The secrets manifest (`.dev.vars.example`) and the temporary secrets file come from the working directory, not from
|
|
720
745
|
beside the `--config` file.
|
|
746
|
+
- Secrets are gated per stage. `smoo.wrangler.secretStages` in the project's `package.json` maps a declared secret name
|
|
747
|
+
to the stages that require it, and the mapping cuts both ways: a listed stage REFUSES to deploy when that secret has
|
|
748
|
+
no value in the environment and none on the Worker, and an unlisted stage never RECEIVES it even when the deploying
|
|
749
|
+
shell exports one. `preview` matches any `prN`. A name absent from the map is required by every stage.
|
|
750
|
+
- The refusal happens before any Cloudflare mutation and names every missing secret at once, so a first deploy reports
|
|
751
|
+
the whole list instead of one name per attempt. Values never appear in a message or in argv.
|
|
752
|
+
- Both directions are load-bearing, and neither is a lint. `--secrets-file` applies additively — wrangler deletes
|
|
753
|
+
nothing it is not told about — so a secret introduced after a stage's first deploy would otherwise never arrive and
|
|
754
|
+
the Worker would keep serving without it, silently. And a token scoped to preview stages, exported by a shell that
|
|
755
|
+
also deploys production, would otherwise install a test-only capability into production.
|
|
721
756
|
- Migrations run only for the D1 bindings that declare a `migrations_dir`.
|
|
722
757
|
- For a `prN` stage, an R2 bucket or D1 database whose name has no exact `staging` segment is refused, before any
|
|
723
758
|
Cloudflare resource is created: reusing the name verbatim would share staging's data with the pull request.
|
package/dist/cli.js
CHANGED
|
@@ -427,22 +427,25 @@ function buildProgram() {
|
|
|
427
427
|
.description('Reconcile declared secrets: what Workers need, what workflows pass, what the repository holds');
|
|
428
428
|
secrets
|
|
429
429
|
.command('status')
|
|
430
|
-
.description('Show every declared secret and refuse when a workflow passes one
|
|
431
|
-
.option('--repo <owner/name>',
|
|
430
|
+
.description('Show every declared secret with the scopes holding it, and refuse when a workflow passes one none has')
|
|
431
|
+
.option('-R, --repo <owner/name|remote>', "repository or remote name; defaults to the current branch's upstream remote")
|
|
432
|
+
.option('--env <environment>', "also read this GitHub Environment; its value takes precedence over the repository's for a job bound to it")
|
|
432
433
|
.action(async (options) => {
|
|
433
434
|
process.exitCode = secretsStatus(await findRepoRoot(), options);
|
|
434
435
|
});
|
|
435
436
|
secrets
|
|
436
|
-
.command('set
|
|
437
|
-
.description('Set
|
|
438
|
-
.option('--repo <owner/name>',
|
|
437
|
+
.command('set [name]')
|
|
438
|
+
.description('Set secrets from pasted values; with no name, prompts for every secret the target scope still lacks')
|
|
439
|
+
.option('-R, --repo <owner/name|remote>', "repository or remote name; defaults to the current branch's upstream remote")
|
|
440
|
+
.option('--env <environment>', "write into this GitHub Environment; its value takes precedence over the repository's for a job bound to it")
|
|
439
441
|
.action(async (name, options) => {
|
|
440
|
-
process.exitCode = await secretsSet(name, options);
|
|
442
|
+
process.exitCode = await secretsSet(await findRepoRoot(), name, options);
|
|
441
443
|
});
|
|
442
444
|
secrets
|
|
443
445
|
.command('sync')
|
|
444
446
|
.description('Push every secret smoo.secrets can fetch locally to the repository')
|
|
445
|
-
.option('--repo <owner/name>',
|
|
447
|
+
.option('-R, --repo <owner/name|remote>', "repository or remote name; defaults to the current branch's upstream remote")
|
|
448
|
+
.option('--env <environment>', "write into this GitHub Environment; its value takes precedence over the repository's for a job bound to it")
|
|
446
449
|
.action(async (options) => {
|
|
447
450
|
process.exitCode = await secretsSync(await findRepoRoot(), options);
|
|
448
451
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/github-ci/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EAGvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,KAAK,eAAe,EAA8D,MAAM,sBAAsB,CAAC;AAExH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE;QACX,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,YAAY,CAAC,EAAE;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE;YACL,IAAI,CAAC,EAAE;gBACL,SAAS,CAAC,EAAE,MAAM,CAAC;aACpB,CAAC;SACH,CAAC;KACH,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/github-ci/index.ts"],"names":[],"mappings":"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"}
|
package/dist/github-ci/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { appendFile } from 'node:fs/promises';
|
|
|
4
4
|
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
5
5
|
import { $ } from 'bun';
|
|
6
6
|
import typia from 'typia';
|
|
7
|
-
import {
|
|
7
|
+
import { PERMANENT_DEPLOY_TAG, STAGING_DEPLOY_TAG, stageDeploysProject } from '../lib/deploy-tags.js';
|
|
8
8
|
import { ciPushBranches, isNonEmpty, parseStringArrayText, readSmooGithub, } from '../lib/json.js';
|
|
9
9
|
import { decode, printCommandOutput, run, runStatus, runText } from '../lib/run.js';
|
|
10
10
|
import { readProjectTargets } from '../nx/index.js';
|
|
@@ -25,19 +25,11 @@ const isGithubDeployment = (() => {
|
|
|
25
25
|
return input => "object" === typeof input && null !== input && _io0(input);
|
|
26
26
|
})();
|
|
27
27
|
const isNxProjectDeployTarget = (() => {
|
|
28
|
-
const
|
|
29
|
-
const _io0 = input => true && (undefined === input.targets || "object" === typeof input.targets && null !== input.targets && false === Array.isArray(input.targets) && _io1(input.targets));
|
|
30
|
-
const _io1 = input => undefined === input.deploy || "object" === typeof input.deploy && null !== input.deploy && false === Array.isArray(input.deploy) && _io2(input.deploy);
|
|
31
|
-
const _io2 = input => _ip0(input) && (undefined === input.options || "object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) && _io3(input.options));
|
|
32
|
-
const _io3 = input => _ip0(input);
|
|
28
|
+
const _io0 = input => undefined === input.tags || Array.isArray(input.tags) && input.tags.every(elem => "string" === typeof elem);
|
|
33
29
|
return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
|
|
34
30
|
})();
|
|
35
31
|
const parseNxProjectDeployTarget = (() => {
|
|
36
|
-
const
|
|
37
|
-
const _io0 = input => true && (undefined === input.targets || "object" === typeof input.targets && null !== input.targets && false === Array.isArray(input.targets) && _io1(input.targets));
|
|
38
|
-
const _io1 = input => undefined === input.deploy || "object" === typeof input.deploy && null !== input.deploy && false === Array.isArray(input.deploy) && _io2(input.deploy);
|
|
39
|
-
const _io2 = input => _ip0(input) && (undefined === input.options || "object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) && _io3(input.options));
|
|
40
|
-
const _io3 = input => _ip0(input);
|
|
32
|
+
const _io0 = input => undefined === input.tags || Array.isArray(input.tags) && input.tags.every(elem => "string" === typeof elem);
|
|
41
33
|
const __is = input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
|
|
42
34
|
return input => { input = JSON.parse(input); return __is(input) ? input : null; };
|
|
43
35
|
})();
|
|
@@ -434,17 +426,11 @@ export async function selectStageDeployProjects(candidates, stage, requireTag, l
|
|
|
434
426
|
const definition = await loadProject(project);
|
|
435
427
|
if (!isNxProjectDeployTarget(definition))
|
|
436
428
|
continue;
|
|
437
|
-
const
|
|
438
|
-
const commandValue = deploy?.options?.command ?? deploy?.command;
|
|
439
|
-
const tags = Array.isArray(definition.tags)
|
|
440
|
-
? definition.tags.filter((tag) => typeof tag === 'string')
|
|
441
|
-
: [];
|
|
442
|
-
const isStageDerived = isStageDerivedDeploy(tags, typeof commandValue === 'string' ? commandValue : undefined);
|
|
443
|
-
const isStagingOnly = tags.includes(STAGING_DEPLOY_TAG);
|
|
429
|
+
const tags = definition.tags;
|
|
444
430
|
// A required tag narrows the stage rules; it never selects a project they exclude.
|
|
445
|
-
if (requireTag && !tags
|
|
431
|
+
if (requireTag && !tags?.includes(requireTag))
|
|
446
432
|
continue;
|
|
447
|
-
if (
|
|
433
|
+
if (stageDeploysProject(tags, stage)) {
|
|
448
434
|
selected.push(project);
|
|
449
435
|
}
|
|
450
436
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DeploymentStage } from '../wrangler/stage.js';
|
|
1
2
|
/** Deployed on staging only: infrastructure the pull-request stages share. */
|
|
2
3
|
export declare const STAGING_DEPLOY_TAG = "staging-deploy-target";
|
|
3
4
|
/** Excluded from every stage deploy; deployed outside the stage flow. */
|
|
@@ -5,8 +6,21 @@ export declare const PERMANENT_DEPLOY_TAG = "permanent-deploy-target";
|
|
|
5
6
|
/** Opts a project into the generated production-on-push deploy job. */
|
|
6
7
|
export declare const PRODUCTION_PUSH_DEPLOY_TAG = "production-push-deploy-target";
|
|
7
8
|
/**
|
|
8
|
-
* Whether
|
|
9
|
-
*
|
|
9
|
+
* Whether the stage flow deploys a project, from its TAGS alone — the one rule
|
|
10
|
+
* CI selection and workflow generation both read, so a generated deploy job and
|
|
11
|
+
* the projects that job deploys cannot disagree.
|
|
12
|
+
*
|
|
13
|
+
* Having a `deploy` target is not that answer. A deploy target says a project
|
|
14
|
+
* CAN be deployed; only a tag says CI does it. A published library ships a
|
|
15
|
+
* wrangler manifest to document a Durable Object binding for its consumers, and
|
|
16
|
+
* that manifest is structurally identical to a deployable worker's — so the
|
|
17
|
+
* plugin infers `deploy` from one, and a repository that deploys nothing from CI
|
|
18
|
+
* would otherwise generate a deploy job, with cloud credentials, for it.
|
|
19
|
+
*
|
|
20
|
+
* The three tags compose as precedence, not as a set: `permanent` excludes a
|
|
21
|
+
* project from every stage, `staging` narrows it to one, `stage` generalizes it
|
|
22
|
+
* to all. `production-push` selects WITHIN this answer (see `--select-tag`) and
|
|
23
|
+
* never widens it.
|
|
10
24
|
*/
|
|
11
|
-
export declare function
|
|
25
|
+
export declare function stageDeploysProject(tags: string[] | undefined, stage: DeploymentStage): boolean;
|
|
12
26
|
//# sourceMappingURL=deploy-tags.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-tags.d.ts","sourceRoot":"","sources":["../../src/lib/deploy-tags.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy-tags.d.ts","sourceRoot":"","sources":["../../src/lib/deploy-tags.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAK5D,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAE9D,uEAAuE;AACvE,eAAO,MAAM,0BAA0B,kCAAkC,CAAC;AAE1E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAK/F"}
|
package/dist/lib/deploy-tags.js
CHANGED
|
@@ -9,9 +9,28 @@ export const PERMANENT_DEPLOY_TAG = 'permanent-deploy-target';
|
|
|
9
9
|
/** Opts a project into the generated production-on-push deploy job. */
|
|
10
10
|
export const PRODUCTION_PUSH_DEPLOY_TAG = 'production-push-deploy-target';
|
|
11
11
|
/**
|
|
12
|
-
* Whether
|
|
13
|
-
*
|
|
12
|
+
* Whether the stage flow deploys a project, from its TAGS alone — the one rule
|
|
13
|
+
* CI selection and workflow generation both read, so a generated deploy job and
|
|
14
|
+
* the projects that job deploys cannot disagree.
|
|
15
|
+
*
|
|
16
|
+
* Having a `deploy` target is not that answer. A deploy target says a project
|
|
17
|
+
* CAN be deployed; only a tag says CI does it. A published library ships a
|
|
18
|
+
* wrangler manifest to document a Durable Object binding for its consumers, and
|
|
19
|
+
* that manifest is structurally identical to a deployable worker's — so the
|
|
20
|
+
* plugin infers `deploy` from one, and a repository that deploys nothing from CI
|
|
21
|
+
* would otherwise generate a deploy job, with cloud credentials, for it.
|
|
22
|
+
*
|
|
23
|
+
* The three tags compose as precedence, not as a set: `permanent` excludes a
|
|
24
|
+
* project from every stage, `staging` narrows it to one, `stage` generalizes it
|
|
25
|
+
* to all. `production-push` selects WITHIN this answer (see `--select-tag`) and
|
|
26
|
+
* never widens it.
|
|
14
27
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
28
|
+
export function stageDeploysProject(tags, stage) {
|
|
29
|
+
if (tags === undefined)
|
|
30
|
+
return false;
|
|
31
|
+
if (tags.includes(PERMANENT_DEPLOY_TAG))
|
|
32
|
+
return false;
|
|
33
|
+
if (tags.includes(STAGING_DEPLOY_TAG))
|
|
34
|
+
return stage === 'staging';
|
|
35
|
+
return tags.includes(STAGE_DEPLOY_TAG);
|
|
17
36
|
}
|
|
@@ -10,6 +10,11 @@ export declare enum CiWorkflowStepKind {
|
|
|
10
10
|
BrowserTests = "browser-tests",
|
|
11
11
|
Lint = "lint",
|
|
12
12
|
UnitTests = "unit-tests",
|
|
13
|
+
CrossToolchainPreflight = "cross-toolchain-preflight",
|
|
14
|
+
CrossTestArchives = "cross-test-archives",
|
|
15
|
+
UploadCrossTestArchives = "upload-cross-test-archives",
|
|
16
|
+
DownloadCrossTestArchives = "download-cross-test-archives",
|
|
17
|
+
CrossTests = "cross-tests",
|
|
13
18
|
ManagedFilesCheck = "managed-files-check",
|
|
14
19
|
ManagedFilesDispatch = "managed-files-dispatch",
|
|
15
20
|
Deploy = "deploy",
|
|
@@ -34,6 +39,22 @@ export interface DeployStepSecretConfig {
|
|
|
34
39
|
/** Extra deploy-step secrets, env var name → repository secret name. */
|
|
35
40
|
deploySecrets?: Record<string, string>;
|
|
36
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* One foreign target triple's test archive: what the Linux job cross-builds and
|
|
44
|
+
* a native runner executes.
|
|
45
|
+
*
|
|
46
|
+
* Both fields are DERIVED, never hand-written: the triple comes from the cargo
|
|
47
|
+
* workspace's `[workspace.metadata.smoothbricks.test] cross-targets`, and the
|
|
48
|
+
* path is where the inferred `cargo-cross-test-archive-<triple>` target
|
|
49
|
+
* declares its output. That is why this is not a `smoo.github` key — a triple
|
|
50
|
+
* repeated in CI config could name an archive the graph cannot build, and the
|
|
51
|
+
* workflow would only find out on the runner.
|
|
52
|
+
*/
|
|
53
|
+
export interface CiCrossTestArchive {
|
|
54
|
+
triple: string;
|
|
55
|
+
/** Repository-relative path of the archive file, from the target's output. */
|
|
56
|
+
path: string;
|
|
57
|
+
}
|
|
37
58
|
export interface CiWorkflowDefinitionOptions extends DeployStepSecretConfig {
|
|
38
59
|
actionsProvider?: PackageSmooGithub['actionsProvider'];
|
|
39
60
|
deploy: boolean;
|
|
@@ -75,6 +96,41 @@ export interface CiWorkflowDefinitionOptions extends DeployStepSecretConfig {
|
|
|
75
96
|
e2eSecrets?: Record<string, string>;
|
|
76
97
|
/** Emit the production-on-push job (some project carries PRODUCTION_PUSH_DEPLOY_TAG). */
|
|
77
98
|
productionOnPush?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Cross-built test archives, one per foreign target triple the cargo
|
|
101
|
+
* workspace declares. The Linux job builds and uploads each one; a second job
|
|
102
|
+
* downloads the `*-apple-darwin` ones and EXECUTES them on `macosRunsOn`,
|
|
103
|
+
* which is how a cross compile gets proved by running rather than by linking.
|
|
104
|
+
*
|
|
105
|
+
* Empty or absent renders exactly what a repository without cross archives
|
|
106
|
+
* renders — no step, no job, no numbering shift.
|
|
107
|
+
*/
|
|
108
|
+
crossTestArchives?: readonly CiCrossTestArchive[];
|
|
109
|
+
/**
|
|
110
|
+
* macOS runner labels for the darwin execution job
|
|
111
|
+
* (`smoo.github.macosRunsOn`, the same labels the publish workflow's macOS
|
|
112
|
+
* legs use). Default macos-latest.
|
|
113
|
+
*
|
|
114
|
+
* A self-hosted label here is one specific machine. That machine being asleep
|
|
115
|
+
* does not skip the job: it queues, and the run FAILS on the job's
|
|
116
|
+
* `timeout-minutes`. That is the trade this opt-in buys — a red run when the
|
|
117
|
+
* Mac is unreachable, in exchange for the darwin binaries being tested by
|
|
118
|
+
* execution at all. `continue-on-error` would buy the green back by making
|
|
119
|
+
* the job unable to report anything, which is worse than not having it.
|
|
120
|
+
*/
|
|
121
|
+
macosRunsOn?: string | string[];
|
|
122
|
+
/**
|
|
123
|
+
* Declared Linux cross-platform producer (`smoo.github.platformProducer`):
|
|
124
|
+
* the toolchain preflight and the environment a foreign-target build needs on
|
|
125
|
+
* a Linux runner. The publish workflow already renders this declaration for
|
|
126
|
+
* its cross artifact job, so the cross test archive reuses it rather than
|
|
127
|
+
* carrying a second copy of one repository's SDK and linker setup.
|
|
128
|
+
*
|
|
129
|
+
* Rendered STEP-scoped here, never as job env: this environment turns the
|
|
130
|
+
* repository's native producers into cross producers, and Validate's other
|
|
131
|
+
* steps are host builds that must stay host builds.
|
|
132
|
+
*/
|
|
133
|
+
platformProducer?: PackageSmooGithub['platformProducer'];
|
|
78
134
|
}
|
|
79
135
|
export declare function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkflowStep[];
|
|
80
136
|
export declare function renderCiWorkflowYaml(options: CiWorkflowDefinitionOptions): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ci-workflow.d.ts","sourceRoot":"","sources":["../../src/monorepo/ci-workflow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ci-workflow.d.ts","sourceRoot":"","sources":["../../src/monorepo/ci-workflow.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
|
+
import { cargoCrossTestArchiveFile, cargoCrossTestArchiveTargetName, cargoCrossTestTargetName, } from '@smoothbricks/nx-plugin/cross-check-policy';
|
|
2
3
|
import { PRODUCTION_PUSH_DEPLOY_TAG } from '../lib/deploy-tags.js';
|
|
3
4
|
import { renderRunsOnLine } from './github-runs-on.js';
|
|
4
5
|
export var CiWorkflowStepKind;
|
|
@@ -13,6 +14,11 @@ export var CiWorkflowStepKind;
|
|
|
13
14
|
CiWorkflowStepKind["BrowserTests"] = "browser-tests";
|
|
14
15
|
CiWorkflowStepKind["Lint"] = "lint";
|
|
15
16
|
CiWorkflowStepKind["UnitTests"] = "unit-tests";
|
|
17
|
+
CiWorkflowStepKind["CrossToolchainPreflight"] = "cross-toolchain-preflight";
|
|
18
|
+
CiWorkflowStepKind["CrossTestArchives"] = "cross-test-archives";
|
|
19
|
+
CiWorkflowStepKind["UploadCrossTestArchives"] = "upload-cross-test-archives";
|
|
20
|
+
CiWorkflowStepKind["DownloadCrossTestArchives"] = "download-cross-test-archives";
|
|
21
|
+
CiWorkflowStepKind["CrossTests"] = "cross-tests";
|
|
16
22
|
CiWorkflowStepKind["ManagedFilesCheck"] = "managed-files-check";
|
|
17
23
|
CiWorkflowStepKind["ManagedFilesDispatch"] = "managed-files-dispatch";
|
|
18
24
|
CiWorkflowStepKind["Deploy"] = "deploy";
|
|
@@ -33,6 +39,15 @@ export function defineCiWorkflow(options) {
|
|
|
33
39
|
steps.push({ kind: CiWorkflowStepKind.Deploy, name: '🚀 Deploy Stage' });
|
|
34
40
|
}
|
|
35
41
|
steps.push({ kind: CiWorkflowStepKind.Lint, name: '🔍 Lint' }, { kind: CiWorkflowStepKind.UnitTests, name: '🧪 Unit Tests' });
|
|
42
|
+
if (options.crossTestArchives?.length) {
|
|
43
|
+
if (options.platformProducer !== undefined) {
|
|
44
|
+
steps.push({
|
|
45
|
+
kind: CiWorkflowStepKind.CrossToolchainPreflight,
|
|
46
|
+
name: 'Check cross-platform toolchain prerequisites',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
steps.push({ kind: CiWorkflowStepKind.CrossTestArchives, name: '🎯 Build cross-target test archives' }, { kind: CiWorkflowStepKind.UploadCrossTestArchives, name: '📤 Upload cross-target test archives' });
|
|
50
|
+
}
|
|
36
51
|
if (options.browserTests) {
|
|
37
52
|
steps.push({ kind: CiWorkflowStepKind.BrowserTests, name: '🌐 Browser Tests' });
|
|
38
53
|
}
|
|
@@ -44,6 +59,7 @@ export function renderCiWorkflowYaml(options) {
|
|
|
44
59
|
return [
|
|
45
60
|
renderCiWorkflowHeader(options),
|
|
46
61
|
renderCiWorkflowSteps(steps, options),
|
|
62
|
+
renderCrossTestExecutionJob(options),
|
|
47
63
|
renderE2eDeploymentJob(options),
|
|
48
64
|
renderProductionDeployJob(options),
|
|
49
65
|
].join('');
|
|
@@ -216,6 +232,61 @@ function yamlLinesForStep(step, options) {
|
|
|
216
232
|
return nxSmartStep(step, 'lint', 'Lint');
|
|
217
233
|
case CiWorkflowStepKind.UnitTests:
|
|
218
234
|
return nxSmartStep(step, 'test', 'Unit Tests');
|
|
235
|
+
case CiWorkflowStepKind.CrossToolchainPreflight:
|
|
236
|
+
// The repository's own preflight, the same one the publish workflow's
|
|
237
|
+
// cross artifact job runs — an SDK to provision, a linker to check.
|
|
238
|
+
// `working-directory: .` because this workflow's steps default to the
|
|
239
|
+
// direnv shell directory while a preflight is repository-relative.
|
|
240
|
+
return [
|
|
241
|
+
` - name: ${step.name}`,
|
|
242
|
+
' working-directory: .',
|
|
243
|
+
...platformProducerStepEnvLines(options),
|
|
244
|
+
' run: |',
|
|
245
|
+
' set -euo pipefail',
|
|
246
|
+
...crossToolchainPreflight(options).map((line) => ` ${line}`),
|
|
247
|
+
];
|
|
248
|
+
case CiWorkflowStepKind.CrossTestArchives:
|
|
249
|
+
// Cross-COMPILES the workspace's test binaries for each declared foreign
|
|
250
|
+
// triple and packs them; runs nothing. `nx-run-many` rather than
|
|
251
|
+
// `nx-smart`, because an affected-graph selection would skip the archive
|
|
252
|
+
// on a commit that changed nothing Rust — and the sibling job that
|
|
253
|
+
// executes it has no way to build one.
|
|
254
|
+
return [
|
|
255
|
+
` - name: ${step.name}`,
|
|
256
|
+
...platformProducerStepEnvLines(options),
|
|
257
|
+
` run: smoo github-ci nx-run-many --targets "${crossArchiveTargetNames(options).join(',')}"`,
|
|
258
|
+
];
|
|
259
|
+
case CiWorkflowStepKind.UploadCrossTestArchives:
|
|
260
|
+
// retention-days: 1 — the only consumer is the sibling job in this same
|
|
261
|
+
// run, and these archives are the whole workspace's test binaries.
|
|
262
|
+
return artifactStepLines(options.actionsProvider, step.name, 'upload', [
|
|
263
|
+
`name: ${CROSS_TEST_ARCHIVE_ARTIFACT}`,
|
|
264
|
+
'path: |',
|
|
265
|
+
...crossTestArchives(options).map((archive) => ` ${archive.path}`),
|
|
266
|
+
// error, not ignore: a missing archive means the cross build silently
|
|
267
|
+
// produced nothing, and the execute job would then have nothing to run
|
|
268
|
+
// and no reason to say so.
|
|
269
|
+
'if-no-files-found: error',
|
|
270
|
+
'retention-days: 1',
|
|
271
|
+
]);
|
|
272
|
+
case CiWorkflowStepKind.DownloadCrossTestArchives:
|
|
273
|
+
// Restores each archive at the path its own Nx target declares as output,
|
|
274
|
+
// which is where the runner target reads it from. upload-artifact roots
|
|
275
|
+
// the artifact at the least common ancestor of what it uploaded, so the
|
|
276
|
+
// download path is that shared directory.
|
|
277
|
+
return artifactStepLines(options.actionsProvider, step.name, 'download', [
|
|
278
|
+
`name: ${CROSS_TEST_ARCHIVE_ARTIFACT}`,
|
|
279
|
+
`path: ${crossTestArchiveDirectory(options)}`,
|
|
280
|
+
]);
|
|
281
|
+
case CiWorkflowStepKind.CrossTests:
|
|
282
|
+
// EXECUTES, and that is all it can do: every one of these targets is
|
|
283
|
+
// `nextest run --archive-file`, which extracts prebuilt binaries into its
|
|
284
|
+
// own temporary directory. nextest never invokes cargo on this path
|
|
285
|
+
// (measured), so nothing here can compile even if a toolchain is present.
|
|
286
|
+
return [
|
|
287
|
+
` - name: ${step.name}`,
|
|
288
|
+
` run: smoo github-ci nx-run-many --targets "${darwinCrossTestTargetNames(options).join(',')}"`,
|
|
289
|
+
];
|
|
219
290
|
case CiWorkflowStepKind.ManagedFilesCheck:
|
|
220
291
|
// The authoritative drift check is nearly free here (devenv is already
|
|
221
292
|
// up). --warn never fails the job and publishes the step output
|
|
@@ -765,12 +836,13 @@ function renderYamlList(values, spaces) {
|
|
|
765
836
|
const indent = ' '.repeat(spaces);
|
|
766
837
|
return values.map((value) => `${indent}- ${value}`).join('\n');
|
|
767
838
|
}
|
|
768
|
-
function followUpStepNumbers(options) {
|
|
839
|
+
function followUpStepNumbers(options, middleSteps = 1) {
|
|
769
840
|
let number = 3;
|
|
770
841
|
const cargo = options.cargoCredentials !== undefined ? number++ : undefined;
|
|
771
842
|
const sources = options.sourceCheckouts?.length ? number++ : undefined;
|
|
772
843
|
const setup = number++;
|
|
773
|
-
const middle = number
|
|
844
|
+
const middle = number;
|
|
845
|
+
number += middleSteps;
|
|
774
846
|
const cleanup = number++;
|
|
775
847
|
return { cargo, sources, setup, middle, cleanup };
|
|
776
848
|
}
|
|
@@ -794,6 +866,131 @@ function followUpSetupSteps(options, numbers) {
|
|
|
794
866
|
function followUpCleanupStep(numbers) {
|
|
795
867
|
return [{ kind: CiWorkflowStepKind.SaveNixDevenv, name: '🧹 Cleanup and cache Nix/devenv', number: numbers.cleanup }];
|
|
796
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* How long the darwin execution job may take, download included. Generous
|
|
871
|
+
* because the archive is the whole workspace's test binaries, and bounded
|
|
872
|
+
* because the runner behind `macosRunsOn` may be one person's laptop: an
|
|
873
|
+
* unreachable runner queues, and this number is when the run says so.
|
|
874
|
+
*/
|
|
875
|
+
const CROSS_TEST_JOB_TIMEOUT_MINUTES = 30;
|
|
876
|
+
/** Rust names every Apple-desktop triple this way; iOS and simulator do not. */
|
|
877
|
+
const DARWIN_TRIPLE_SUFFIX = '-apple-darwin';
|
|
878
|
+
/** One artifact carrying every triple's archive, scoped to the run that built it. */
|
|
879
|
+
const CROSS_TEST_ARCHIVE_ARTIFACT = `cross-test-archives-${githubExpression('github.run_id')}`;
|
|
880
|
+
/**
|
|
881
|
+
* Executes the cross-built test binaries on a machine where they are native.
|
|
882
|
+
*
|
|
883
|
+
* This job COMPILES NOTHING, and cannot: its only Rust input is the archive the
|
|
884
|
+
* Validate job uploaded, and `nextest run --archive-file` extracts prebuilt
|
|
885
|
+
* binaries rather than invoking cargo (measured: zero cargo invocations). It
|
|
886
|
+
* needs the checkout for the workspace `--workspace-remap .` points at and for
|
|
887
|
+
* Nx to resolve the target, and setup-devenv for `nx` and `cargo-nextest`
|
|
888
|
+
* themselves — the repository's ordinary shell, not a cross toolchain, no SDK,
|
|
889
|
+
* no `rustup target add`.
|
|
890
|
+
*
|
|
891
|
+
* `needs: main` and a success gate rather than `!cancelled()`: an archive that
|
|
892
|
+
* was never uploaded cannot be executed, and a job that starts anyway would
|
|
893
|
+
* fail on a missing artifact and report it as a test failure.
|
|
894
|
+
*
|
|
895
|
+
* Only `*-apple-darwin` archives run here, because macOS runner labels are the
|
|
896
|
+
* only native runner this declaration names. A repository declaring some other
|
|
897
|
+
* foreign triple still gets its archive BUILT — which proves that triple
|
|
898
|
+
* compiles — and no execution job, which is the honest rendering of having
|
|
899
|
+
* nowhere to run it.
|
|
900
|
+
*/
|
|
901
|
+
function renderCrossTestExecutionJob(options) {
|
|
902
|
+
const archives = darwinCrossTestArchives(options);
|
|
903
|
+
if (archives.length === 0)
|
|
904
|
+
return '';
|
|
905
|
+
const numbers = followUpStepNumbers(options, 2);
|
|
906
|
+
const middleSteps = [
|
|
907
|
+
{
|
|
908
|
+
kind: CiWorkflowStepKind.DownloadCrossTestArchives,
|
|
909
|
+
name: '📥 Download cross-target test archives',
|
|
910
|
+
number: numbers.middle,
|
|
911
|
+
},
|
|
912
|
+
{ kind: CiWorkflowStepKind.CrossTests, name: '🧪 Cross-Target Unit Tests', number: numbers.middle + 1 },
|
|
913
|
+
];
|
|
914
|
+
return `
|
|
915
|
+
macos-cross-tests:
|
|
916
|
+
name: Unit Tests (${archives.map((archive) => archive.triple).join(', ')})
|
|
917
|
+
needs: main
|
|
918
|
+
${options.macosRunsOn === undefined ? ' runs-on: macos-latest' : renderRunsOnLine(options.macosRunsOn)}
|
|
919
|
+
timeout-minutes: ${CROSS_TEST_JOB_TIMEOUT_MINUTES}
|
|
920
|
+
if: \${{ needs.main.result == 'success' }}
|
|
921
|
+
env:
|
|
922
|
+
GH_TOKEN: \${{ github.token }}
|
|
923
|
+
${remoteCacheJobEnvLines(options.remoteCache)}${cargoCredentialJobEnvLines(options.cargoCredentials)}${privateNpmReadTokenJobEnv(options)} steps:
|
|
924
|
+
${renderCiWorkflowSteps(followUpSetupSteps(options, numbers), options)}
|
|
925
|
+
${renderCiWorkflowSteps(middleSteps, options)}
|
|
926
|
+
${renderCiWorkflowSteps(followUpCleanupStep(numbers), options)}`;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* The declared archives, checked against the naming the Nx graph would have
|
|
930
|
+
* produced. A path that is not `cargoCrossTestArchiveFile(triple)` under some
|
|
931
|
+
* project root cannot have come from that target, so it is refused here rather
|
|
932
|
+
* than uploaded as an artifact nothing on the other side can find.
|
|
933
|
+
*/
|
|
934
|
+
function crossTestArchives(options) {
|
|
935
|
+
return (options.crossTestArchives ?? []).map((archive) => {
|
|
936
|
+
const suffix = cargoCrossTestArchiveFile(archive.triple);
|
|
937
|
+
const path = archive.path.split('\\').join('/');
|
|
938
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(archive.triple)) {
|
|
939
|
+
throw new Error(`smoo cross test archive triple must be a target triple, got ${JSON.stringify(archive.triple)}`);
|
|
940
|
+
}
|
|
941
|
+
if (path !== suffix && !path.endsWith(`/${suffix}`)) {
|
|
942
|
+
throw new Error(`smoo cross test archive for ${archive.triple} must be a repository-relative ${suffix}, got ${archive.path}`);
|
|
943
|
+
}
|
|
944
|
+
if (path.startsWith('/') || path.split('/').includes('..')) {
|
|
945
|
+
throw new Error(`smoo cross test archive path must stay inside the repository, got ${archive.path}`);
|
|
946
|
+
}
|
|
947
|
+
return { triple: archive.triple, path };
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function crossArchiveTargetNames(options) {
|
|
951
|
+
return crossTestArchives(options).map((archive) => cargoCrossTestArchiveTargetName(archive.triple));
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The declared preflight, as lines. A declaration with nothing in it is a
|
|
955
|
+
* mechanism error, and it surfaces here at render time rather than as a step
|
|
956
|
+
* whose `run:` block is empty.
|
|
957
|
+
*/
|
|
958
|
+
function crossToolchainPreflight(options) {
|
|
959
|
+
const preflight = options.platformProducer?.preflight ?? '';
|
|
960
|
+
if (preflight.trim().length === 0) {
|
|
961
|
+
throw new Error('smoo.github.platformProducer requires a nonempty toolchain preflight command.');
|
|
962
|
+
}
|
|
963
|
+
return preflight.split('\n');
|
|
964
|
+
}
|
|
965
|
+
/** The producer environment, step-scoped, quoted exactly as the publish workflow quotes it. */
|
|
966
|
+
function platformProducerStepEnvLines(options) {
|
|
967
|
+
const entries = Object.entries(options.platformProducer?.env ?? {});
|
|
968
|
+
if (entries.length === 0)
|
|
969
|
+
return [];
|
|
970
|
+
return [' env:', ...entries.map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`)];
|
|
971
|
+
}
|
|
972
|
+
function darwinCrossTestArchives(options) {
|
|
973
|
+
return crossTestArchives(options).filter((archive) => archive.triple.endsWith(DARWIN_TRIPLE_SUFFIX));
|
|
974
|
+
}
|
|
975
|
+
function darwinCrossTestTargetNames(options) {
|
|
976
|
+
return darwinCrossTestArchives(options).map((archive) => cargoCrossTestTargetName(archive.triple));
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Where the download lands. upload-artifact roots an artifact at the least
|
|
980
|
+
* common ancestor of the files it uploaded, so every archive in one artifact
|
|
981
|
+
* has to share a parent directory for the restored layout to be the one the
|
|
982
|
+
* runner targets read — true for one cargo workspace, false the moment a
|
|
983
|
+
* second one also declares cross targets, which is refused here instead of
|
|
984
|
+
* silently restoring the archives one directory too high.
|
|
985
|
+
*/
|
|
986
|
+
function crossTestArchiveDirectory(options) {
|
|
987
|
+
const directories = new Set(darwinCrossTestArchives(options).map((archive) => archive.path.slice(0, archive.path.lastIndexOf('/') + 1) || './'));
|
|
988
|
+
if (directories.size > 1) {
|
|
989
|
+
throw new Error(`smoo cross test archives must share one directory to travel as one artifact, got ${[...directories].join(', ')}`);
|
|
990
|
+
}
|
|
991
|
+
const [directory] = [...directories];
|
|
992
|
+
return (directory ?? './').replace(/\/$/, '');
|
|
993
|
+
}
|
|
797
994
|
function renderE2eDeploymentJob(options) {
|
|
798
995
|
if (!options.e2eDeployment)
|
|
799
996
|
return '';
|