@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
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# macOS-only Linux compile gate.
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
# macOS-only Linux cross-compile gate. This hook is a cache PROBE and nothing
|
|
3
|
+
# else: it reads the nx cache and never compiles. A hit means a real
|
|
4
|
+
# `cargo clippy --target x86_64-unknown-linux-gnu` already passed for exactly
|
|
5
|
+
# this tree, so the push is safe with no toolchain present. Anything else
|
|
6
|
+
# refuses the push and names the one command that fixes it.
|
|
7
|
+
#
|
|
8
|
+
# Why probe-only. The cross-clippy needs the linux-cross C toolchain, which
|
|
9
|
+
# lives in a devenv profile. A hook that entered that profile spent minutes
|
|
10
|
+
# compiling on a push the user expected to take a second, and it did so in an
|
|
11
|
+
# environment they never asked for. Pushing is not the place to discover that
|
|
12
|
+
# the tree has not been compiled for Linux; `bun run check:linux` is.
|
|
13
|
+
#
|
|
14
|
+
# CC_x86_64_unknown_linux_gnu is unset for the probe deliberately. The target's
|
|
15
|
+
# own guard reads it to decide whether the toolchain is present, so leaving it
|
|
16
|
+
# set would let a push made from inside an already-entered linux-cross shell
|
|
17
|
+
# fall through into a real multi-minute compile. It is not a declared input of
|
|
18
|
+
# the target, so unsetting it cannot change the task hash โ a warm entry still
|
|
19
|
+
# hits.
|
|
20
|
+
#
|
|
21
|
+
# Everything in tooling depends only on devenv.nix packages, so this hook also
|
|
22
|
+
# works with no devenv on PATH. It must never pretend a check ran that did not.
|
|
23
23
|
|
|
24
24
|
cd "$(git rev-parse --show-toplevel)"
|
|
25
25
|
TOOLING="$PWD/tooling"
|
|
@@ -31,17 +31,18 @@ case "$(uname -s)" in
|
|
|
31
31
|
*) exit 0 ;;
|
|
32
32
|
esac
|
|
33
33
|
|
|
34
|
-
if nx run-many -t cargo-lint-cross; then
|
|
34
|
+
if env -u CC_x86_64_unknown_linux_gnu nx run-many -t cargo-lint-cross --output-style=static; then
|
|
35
35
|
exit 0
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
-
if has_nix_devenv; then
|
|
39
|
-
exec bun run check:linux
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
38
|
cat >&2 <<'EOF'
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
|
|
40
|
+
pre-push: the Linux cross-compile check is NOT cached for this tree, so this
|
|
41
|
+
push would ship code that has never been compiled for Linux. This hook only
|
|
42
|
+
reads the cache; it does not build. The "needs the linux-cross C toolchain"
|
|
43
|
+
line above is the probe refusing to compile, not a broken toolchain.
|
|
44
|
+
|
|
45
|
+
Run: bun run check:linux
|
|
46
|
+
Wait for it to pass, then push again.
|
|
46
47
|
EOF
|
|
47
48
|
exit 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@arethetypeswrong/core": "^0.18.2",
|
|
67
|
-
"@smoothbricks/nx-plugin": "0.4.
|
|
67
|
+
"@smoothbricks/nx-plugin": "0.4.13",
|
|
68
68
|
"@smoothbricks/validation": "0.1.8",
|
|
69
69
|
"commander": "^14.0.3",
|
|
70
70
|
"make-synchronized": "^0.8.0",
|
package/src/cli.ts
CHANGED
|
@@ -532,23 +532,46 @@ function buildProgram(): Command {
|
|
|
532
532
|
.description('Reconcile declared secrets: what Workers need, what workflows pass, what the repository holds');
|
|
533
533
|
secrets
|
|
534
534
|
.command('status')
|
|
535
|
-
.description(
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
.description(
|
|
536
|
+
'Show every declared secret with the scopes holding it, and refuse when a workflow passes one none has',
|
|
537
|
+
)
|
|
538
|
+
.option(
|
|
539
|
+
'-R, --repo <owner/name|remote>',
|
|
540
|
+
"repository or remote name; defaults to the current branch's upstream remote",
|
|
541
|
+
)
|
|
542
|
+
.option(
|
|
543
|
+
'--env <environment>',
|
|
544
|
+
"also read this GitHub Environment; its value takes precedence over the repository's for a job bound to it",
|
|
545
|
+
)
|
|
546
|
+
.action(async (options: { repo?: string; env?: string }) => {
|
|
538
547
|
process.exitCode = secretsStatus(await findRepoRoot(), options);
|
|
539
548
|
});
|
|
540
549
|
secrets
|
|
541
|
-
.command('set
|
|
542
|
-
.description('Set
|
|
543
|
-
.option(
|
|
544
|
-
|
|
545
|
-
|
|
550
|
+
.command('set [name]')
|
|
551
|
+
.description('Set secrets from pasted values; with no name, prompts for every secret the target scope still lacks')
|
|
552
|
+
.option(
|
|
553
|
+
'-R, --repo <owner/name|remote>',
|
|
554
|
+
"repository or remote name; defaults to the current branch's upstream remote",
|
|
555
|
+
)
|
|
556
|
+
.option(
|
|
557
|
+
'--env <environment>',
|
|
558
|
+
"write into this GitHub Environment; its value takes precedence over the repository's for a job bound to it",
|
|
559
|
+
)
|
|
560
|
+
.action(async (name: string | undefined, options: { repo?: string; env?: string }) => {
|
|
561
|
+
process.exitCode = await secretsSet(await findRepoRoot(), name, options);
|
|
546
562
|
});
|
|
547
563
|
secrets
|
|
548
564
|
.command('sync')
|
|
549
565
|
.description('Push every secret smoo.secrets can fetch locally to the repository')
|
|
550
|
-
.option(
|
|
551
|
-
|
|
566
|
+
.option(
|
|
567
|
+
'-R, --repo <owner/name|remote>',
|
|
568
|
+
"repository or remote name; defaults to the current branch's upstream remote",
|
|
569
|
+
)
|
|
570
|
+
.option(
|
|
571
|
+
'--env <environment>',
|
|
572
|
+
"write into this GitHub Environment; its value takes precedence over the repository's for a job bound to it",
|
|
573
|
+
)
|
|
574
|
+
.action(async (options: { repo?: string; env?: string }) => {
|
|
552
575
|
process.exitCode = await secretsSync(await findRepoRoot(), options);
|
|
553
576
|
});
|
|
554
577
|
|
|
@@ -847,8 +847,11 @@ describe('event-aware stage deployment', () => {
|
|
|
847
847
|
).toThrow(/same-repository/);
|
|
848
848
|
});
|
|
849
849
|
|
|
850
|
-
it('selects stage
|
|
850
|
+
it('selects tagged stage projects plus staging-only infrastructure, and never an untagged one', async () => {
|
|
851
851
|
const definitions: Record<string, unknown> = {
|
|
852
|
+
// A private wrangler project the plugin gave a deploy target and nobody tagged: deployable
|
|
853
|
+
// by hand, invisible to CI. Reading the command instead deployed whatever held a wrangler
|
|
854
|
+
// manifest, including a published library that ships one to document a binding.
|
|
852
855
|
app: {
|
|
853
856
|
targets: {
|
|
854
857
|
deploy: { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
|
|
@@ -875,18 +878,15 @@ describe('event-aware stage deployment', () => {
|
|
|
875
878
|
const loadProject = async (project: string) => definitions[project];
|
|
876
879
|
|
|
877
880
|
await expect(selectStageDeployProjects(candidates, 'staging', undefined, loadProject)).resolves.toEqual([
|
|
878
|
-
'app',
|
|
879
881
|
'app-backend',
|
|
880
882
|
'e2e-mail-capture',
|
|
881
883
|
'website',
|
|
882
884
|
]);
|
|
883
885
|
await expect(selectStageDeployProjects(candidates, 'pr123', undefined, loadProject)).resolves.toEqual([
|
|
884
|
-
'app',
|
|
885
886
|
'app-backend',
|
|
886
887
|
'website',
|
|
887
888
|
]);
|
|
888
889
|
await expect(selectStageDeployProjects(candidates, 'production', undefined, loadProject)).resolves.toEqual([
|
|
889
|
-
'app',
|
|
890
890
|
'app-backend',
|
|
891
891
|
'website',
|
|
892
892
|
]);
|
|
@@ -1303,15 +1303,14 @@ describe('selectStageDeployProjects with a required tag', () => {
|
|
|
1303
1303
|
};
|
|
1304
1304
|
const loadProject = async (project: string) => definitions[project];
|
|
1305
1305
|
|
|
1306
|
-
it('keeps only projects carrying the tag, on top of the stage
|
|
1306
|
+
it('keeps only projects carrying the tag, on top of the stage rules', async () => {
|
|
1307
1307
|
await expect(
|
|
1308
1308
|
selectStageDeployProjects(Object.keys(definitions), 'production', 'production-push-deploy-target', loadProject),
|
|
1309
1309
|
).resolves.toEqual(['website']);
|
|
1310
1310
|
});
|
|
1311
1311
|
|
|
1312
|
-
it('selects the
|
|
1312
|
+
it('selects only the tagged project on every stage, required tag or not', async () => {
|
|
1313
1313
|
await expect(selectStageDeployProjects(Object.keys(definitions), 'pr12', undefined, loadProject)).resolves.toEqual([
|
|
1314
|
-
'app',
|
|
1315
1314
|
'website',
|
|
1316
1315
|
]);
|
|
1317
1316
|
});
|
package/src/github-ci/index.ts
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 {
|
|
9
9
|
ciPushBranches,
|
|
10
10
|
isNonEmpty,
|
|
@@ -35,16 +35,14 @@ export interface GithubActionsEventPayload {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* What a deploy candidate is READ for: its tags. `nx show projects --withTarget
|
|
40
|
+
* deploy` already established the target, and the tags decide the rest, so a
|
|
41
|
+
* project whose `tags` is not a string array fails the parse instead of being
|
|
42
|
+
* silently untagged.
|
|
43
|
+
*/
|
|
38
44
|
interface NxProjectDeployTarget {
|
|
39
|
-
tags?:
|
|
40
|
-
targets?: {
|
|
41
|
-
deploy?: {
|
|
42
|
-
command?: unknown;
|
|
43
|
-
options?: {
|
|
44
|
-
command?: unknown;
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
};
|
|
45
|
+
tags?: string[];
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
const parseGithubActionsEvent = typia.json.createIsParse<GithubActionsEventPayload>();
|
|
@@ -570,16 +568,10 @@ export async function selectStageDeployProjects(
|
|
|
570
568
|
for (const project of candidates) {
|
|
571
569
|
const definition = await loadProject(project);
|
|
572
570
|
if (!isNxProjectDeployTarget(definition)) continue;
|
|
573
|
-
const
|
|
574
|
-
const commandValue = deploy?.options?.command ?? deploy?.command;
|
|
575
|
-
const tags = Array.isArray(definition.tags)
|
|
576
|
-
? definition.tags.filter((tag): tag is string => typeof tag === 'string')
|
|
577
|
-
: [];
|
|
578
|
-
const isStageDerived = isStageDerivedDeploy(tags, typeof commandValue === 'string' ? commandValue : undefined);
|
|
579
|
-
const isStagingOnly = tags.includes(STAGING_DEPLOY_TAG);
|
|
571
|
+
const tags = definition.tags;
|
|
580
572
|
// A required tag narrows the stage rules; it never selects a project they exclude.
|
|
581
|
-
if (requireTag && !tags
|
|
582
|
-
if (
|
|
573
|
+
if (requireTag && !tags?.includes(requireTag)) continue;
|
|
574
|
+
if (stageDeploysProject(tags, stage)) {
|
|
583
575
|
selected.push(project);
|
|
584
576
|
}
|
|
585
577
|
}
|
package/src/lib/deploy-tags.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The Nx tags that drive stage deploys. CI selection (github-ci), workflow generation (monorepo)
|
|
2
2
|
// and the graph readers must agree on these, so they live here rather than with any one of them.
|
|
3
3
|
|
|
4
|
+
import type { DeploymentStage } from '../wrangler/stage.js';
|
|
5
|
+
|
|
4
6
|
/** Deployed on every stage: one command that deploys whichever stage it is given. */
|
|
5
7
|
const STAGE_DEPLOY_TAG = 'stage-deploy-target';
|
|
6
8
|
|
|
@@ -14,9 +16,25 @@ export const PERMANENT_DEPLOY_TAG = 'permanent-deploy-target';
|
|
|
14
16
|
export const PRODUCTION_PUSH_DEPLOY_TAG = 'production-push-deploy-target';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* Whether
|
|
18
|
-
*
|
|
19
|
+
* Whether the stage flow deploys a project, from its TAGS alone โ the one rule
|
|
20
|
+
* CI selection and workflow generation both read, so a generated deploy job and
|
|
21
|
+
* the projects that job deploys cannot disagree.
|
|
22
|
+
*
|
|
23
|
+
* Having a `deploy` target is not that answer. A deploy target says a project
|
|
24
|
+
* CAN be deployed; only a tag says CI does it. A published library ships a
|
|
25
|
+
* wrangler manifest to document a Durable Object binding for its consumers, and
|
|
26
|
+
* that manifest is structurally identical to a deployable worker's โ so the
|
|
27
|
+
* plugin infers `deploy` from one, and a repository that deploys nothing from CI
|
|
28
|
+
* would otherwise generate a deploy job, with cloud credentials, for it.
|
|
29
|
+
*
|
|
30
|
+
* The three tags compose as precedence, not as a set: `permanent` excludes a
|
|
31
|
+
* project from every stage, `staging` narrows it to one, `stage` generalizes it
|
|
32
|
+
* to all. `production-push` selects WITHIN this answer (see `--select-tag`) and
|
|
33
|
+
* never widens it.
|
|
19
34
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
35
|
+
export function stageDeploysProject(tags: string[] | undefined, stage: DeploymentStage): boolean {
|
|
36
|
+
if (tags === undefined) return false;
|
|
37
|
+
if (tags.includes(PERMANENT_DEPLOY_TAG)) return false;
|
|
38
|
+
if (tags.includes(STAGING_DEPLOY_TAG)) return stage === 'staging';
|
|
39
|
+
return tags.includes(STAGE_DEPLOY_TAG);
|
|
22
40
|
}
|
|
@@ -7,6 +7,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { format } from 'prettier';
|
|
10
|
+
import typia from 'typia';
|
|
10
11
|
import type { PackageCargoGitOrigin } from '../../lib/json.js';
|
|
11
12
|
import {
|
|
12
13
|
type CiWorkflowDefinitionOptions,
|
|
@@ -898,3 +899,163 @@ describe('renderCiWorkflowYaml with deploy configuration', () => {
|
|
|
898
899
|
expect(e2eJob).not.toContain('CARGO_REGISTRIES_EXAMPLE_TOKEN');
|
|
899
900
|
});
|
|
900
901
|
});
|
|
902
|
+
|
|
903
|
+
describe('renderCiWorkflowYaml with cross-built test archives', () => {
|
|
904
|
+
const darwin = {
|
|
905
|
+
triple: 'aarch64-apple-darwin',
|
|
906
|
+
path: 'target/nextest/archive-aarch64-apple-darwin.tar.zst',
|
|
907
|
+
};
|
|
908
|
+
const declared = options({
|
|
909
|
+
runsOn: [...nixosRunsOn],
|
|
910
|
+
macosRunsOn: ['macos-arm64', 'self-hosted'],
|
|
911
|
+
crossTestArchives: [darwin],
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('renders nothing for a repository that declares no cross archives', () => {
|
|
915
|
+
const bare = renderCiWorkflowYaml(options({ runsOn: [...nixosRunsOn] }));
|
|
916
|
+
|
|
917
|
+
// An empty declaration is the same repository as an absent one: no step, no
|
|
918
|
+
// job, and above all no renumbering of the steps that were already there.
|
|
919
|
+
expect(renderCiWorkflowYaml(options({ runsOn: [...nixosRunsOn], crossTestArchives: [] }))).toBe(bare);
|
|
920
|
+
expect(bare).not.toContain('macos-cross-tests');
|
|
921
|
+
expect(bare).not.toContain('cross-target test archives');
|
|
922
|
+
expect(bare).not.toContain('cargo-cross-test');
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('builds and uploads each archive in Validate, then executes it in a job that needs Validate', () => {
|
|
926
|
+
const rendered = renderCiWorkflowYaml(declared);
|
|
927
|
+
const workflow = typia.assert<{
|
|
928
|
+
jobs: Record<string, { needs?: string; 'runs-on'?: unknown; 'timeout-minutes'?: number; steps: unknown[] }>;
|
|
929
|
+
}>(Bun.YAML.parse(rendered));
|
|
930
|
+
const validate = workflow.jobs.main;
|
|
931
|
+
const execute = workflow.jobs['macos-cross-tests'];
|
|
932
|
+
|
|
933
|
+
expect(execute?.needs).toBe('main');
|
|
934
|
+
expect(execute?.['runs-on']).toEqual(['macos-arm64', 'self-hosted']);
|
|
935
|
+
expect(execute?.['timeout-minutes']).toBe(30);
|
|
936
|
+
// The Linux job COMPILES the darwin binaries; that is what proves the cross
|
|
937
|
+
// build works at all.
|
|
938
|
+
expect(rendered).toContain(
|
|
939
|
+
'run: smoo github-ci nx-run-many --targets "cargo-cross-test-archive-aarch64-apple-darwin"',
|
|
940
|
+
);
|
|
941
|
+
expect(JSON.stringify(validate?.steps)).toContain('cross-test-archives-${{ github.run_id }}');
|
|
942
|
+
// ...and the macOS job only RUNS them: every step is checkout, the shell,
|
|
943
|
+
// the download, the archive run, or the cache save. Nothing invokes cargo,
|
|
944
|
+
// a toolchain install, or an SDK.
|
|
945
|
+
const executeSteps = execute?.steps ?? [];
|
|
946
|
+
expect(executeSteps).toContainEqual({
|
|
947
|
+
name: '๐งช Cross-Target Unit Tests',
|
|
948
|
+
run: 'smoo github-ci nx-run-many --targets "cargo-cross-test-aarch64-apple-darwin"',
|
|
949
|
+
});
|
|
950
|
+
const executeText = JSON.stringify(executeSteps);
|
|
951
|
+
expect(executeText).toContain('cross-test-archives-${{ github.run_id }}');
|
|
952
|
+
expect(executeText).toContain('actions/download-artifact');
|
|
953
|
+
expect(executeText).not.toContain('cargo ');
|
|
954
|
+
expect(executeText).not.toContain('rustup');
|
|
955
|
+
expect(executeText).not.toContain('cargo-cross-test-archive');
|
|
956
|
+
expect(executeText).not.toContain('--target build');
|
|
957
|
+
expect(executeText).not.toContain('SDK');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('reuses the declared cross producer for the archive step, step-scoped', () => {
|
|
961
|
+
const rendered = renderCiWorkflowYaml(
|
|
962
|
+
options({
|
|
963
|
+
...declared,
|
|
964
|
+
platformProducer: {
|
|
965
|
+
kind: 'linux-cross',
|
|
966
|
+
preflight: 'sh scripts/prepare-macos-sdk.sh',
|
|
967
|
+
env: { ACME_CROSS: '1', SDKROOT: '${{ runner.temp }}/apple-sdk/MacOSX.sdk' },
|
|
968
|
+
},
|
|
969
|
+
}),
|
|
970
|
+
);
|
|
971
|
+
const validate = rendered.slice(0, rendered.indexOf(' macos-cross-tests:'));
|
|
972
|
+
|
|
973
|
+
expect(validate).toContain('- name: Check cross-platform toolchain prerequisites');
|
|
974
|
+
expect(validate).toContain(' working-directory: .\n');
|
|
975
|
+
expect(validate).toContain(' set -euo pipefail\n sh scripts/prepare-macos-sdk.sh');
|
|
976
|
+
// Step-scoped, so Validate's host builds stay host builds: the pair appears
|
|
977
|
+
// once per cross step and never in the job's own env block.
|
|
978
|
+
expect(validate.match(/ACME_CROSS: "1"/g)).toHaveLength(2);
|
|
979
|
+
const jobEnv = validate.slice(validate.indexOf(' env:'), validate.indexOf(' steps:'));
|
|
980
|
+
expect(jobEnv).not.toContain('ACME_CROSS');
|
|
981
|
+
expect(jobEnv).not.toContain('SDKROOT');
|
|
982
|
+
// A declared producer with no preflight is a mechanism error at render time.
|
|
983
|
+
expect(() =>
|
|
984
|
+
renderCiWorkflowYaml(options({ ...declared, platformProducer: { kind: 'linux-cross', preflight: ' ' } })),
|
|
985
|
+
).toThrow('nonempty toolchain preflight');
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('numbers the execute job download before the run and keeps the cleanup anchor after both', () => {
|
|
989
|
+
const execute = crossTestJob(renderCiWorkflowYaml(declared));
|
|
990
|
+
|
|
991
|
+
// Checkout 2, setup-devenv 3, download 4, run 5, cleanup 6.
|
|
992
|
+
expect(execute).toContain('# Step 4\n - name: ๐ฅ Download cross-target test archives');
|
|
993
|
+
expect(execute).toContain('# Step 5\n - name: ๐งช Cross-Target Unit Tests');
|
|
994
|
+
expect(execute).toContain('# Step 6');
|
|
995
|
+
expect(execute).toContain('uses: ./.github/actions/save-nix-devenv');
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('builds a non-darwin triple without an execution job it has no runner for', () => {
|
|
999
|
+
const rendered = renderCiWorkflowYaml(
|
|
1000
|
+
options({
|
|
1001
|
+
crossTestArchives: [
|
|
1002
|
+
{ triple: 'x86_64-unknown-linux-musl', path: 'target/nextest/archive-x86_64-unknown-linux-musl.tar.zst' },
|
|
1003
|
+
],
|
|
1004
|
+
}),
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
expect(rendered).toContain(
|
|
1008
|
+
'run: smoo github-ci nx-run-many --targets "cargo-cross-test-archive-x86_64-unknown-linux-musl"',
|
|
1009
|
+
);
|
|
1010
|
+
expect(rendered).not.toContain('macos-cross-tests');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('downloads a nested cargo workspace archive back to the directory its target writes', () => {
|
|
1014
|
+
const execute = crossTestJob(
|
|
1015
|
+
renderCiWorkflowYaml(
|
|
1016
|
+
options({
|
|
1017
|
+
crossTestArchives: [{ ...darwin, path: `packages/ferris/${darwin.path}` }],
|
|
1018
|
+
}),
|
|
1019
|
+
),
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
expect(execute).toContain('path: packages/ferris/target/nextest');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('refuses declarations the Nx graph could not have produced, at render time', () => {
|
|
1026
|
+
expect(() =>
|
|
1027
|
+
renderCiWorkflowYaml(options({ crossTestArchives: [{ ...darwin, path: 'target/nextest/archive.tar.zst' }] })),
|
|
1028
|
+
).toThrow('must be a repository-relative target/nextest/archive-aarch64-apple-darwin.tar.zst');
|
|
1029
|
+
expect(() =>
|
|
1030
|
+
renderCiWorkflowYaml(options({ crossTestArchives: [{ ...darwin, path: `../${darwin.path}` }] })),
|
|
1031
|
+
).toThrow('must stay inside the repository');
|
|
1032
|
+
expect(() =>
|
|
1033
|
+
renderCiWorkflowYaml(
|
|
1034
|
+
options({ crossTestArchives: [{ triple: 'Bad Triple; rm -rf /', path: 'target/nextest/x.tar.zst' }] }),
|
|
1035
|
+
),
|
|
1036
|
+
).toThrow('must be a target triple');
|
|
1037
|
+
// Two cargo workspaces cannot share one artifact: upload-artifact roots it
|
|
1038
|
+
// at the least common ancestor, so the restored paths would be wrong for
|
|
1039
|
+
// both. Refuse here rather than on the runner.
|
|
1040
|
+
expect(() =>
|
|
1041
|
+
renderCiWorkflowYaml(
|
|
1042
|
+
options({
|
|
1043
|
+
crossTestArchives: [
|
|
1044
|
+
darwin,
|
|
1045
|
+
{ triple: 'x86_64-apple-darwin', path: `packages/ferris/${cargoArchive('x86_64-apple-darwin')}` },
|
|
1046
|
+
],
|
|
1047
|
+
}),
|
|
1048
|
+
),
|
|
1049
|
+
).toThrow('must share one directory');
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
function crossTestJob(rendered: string): string {
|
|
1054
|
+
const start = rendered.indexOf(' macos-cross-tests:');
|
|
1055
|
+
expect(start).toBeGreaterThan(-1);
|
|
1056
|
+
return rendered.slice(start);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function cargoArchive(triple: string): string {
|
|
1060
|
+
return `target/nextest/archive-${triple}.tar.zst`;
|
|
1061
|
+
}
|