@smoothbricks/cli 0.11.16 → 0.11.18
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 +65 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +47 -0
- package/dist/github-ci/index.d.ts +1 -1
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +7 -10
- package/dist/lib/secret-names.d.ts +35 -0
- package/dist/lib/secret-names.d.ts.map +1 -0
- package/dist/lib/secret-names.js +61 -0
- 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 +214 -8
- package/dist/monorepo/managed-files.d.ts +23 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +39 -1
- package/dist/monorepo/publish-workflow.d.ts +10 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +58 -20
- 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 +84 -0
- package/dist/secrets/commands.d.ts.map +1 -0
- package/dist/secrets/commands.js +375 -0
- package/dist/secrets/index.d.ts +103 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +255 -0
- 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 +13 -2
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +89 -78
- package/dist/wrangler/deployed-version.d.ts +44 -0
- package/dist/wrangler/deployed-version.d.ts.map +1 -0
- package/dist/wrangler/deployed-version.js +87 -0
- package/dist/wrangler/live-version.d.ts +146 -0
- package/dist/wrangler/live-version.d.ts.map +1 -0
- package/dist/wrangler/live-version.js +326 -0
- 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 +69 -1
- package/src/github-ci/index.test.ts +92 -8
- package/src/github-ci/index.ts +7 -10
- package/src/lib/secret-names.ts +70 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +188 -2
- package/src/monorepo/__tests__/publish-workflow.test.ts +33 -15
- package/src/monorepo/ci-workflow.ts +294 -8
- package/src/monorepo/managed-files.test.ts +28 -1
- package/src/monorepo/managed-files.ts +56 -2
- package/src/monorepo/package-policy.test.ts +1 -1
- package/src/monorepo/publish-workflow.ts +65 -19
- package/src/monorepo/secret-references.test.ts +1 -1
- package/src/release/__tests__/github-release.test.ts +8 -4
- package/src/release/__tests__/private-npm-status.test.ts +2 -2
- 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 +412 -0
- package/src/secrets/index.test.ts +209 -0
- package/src/secrets/index.ts +287 -0
- 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 +448 -23
- package/src/wrangler/deploy-stage.ts +142 -83
- package/src/wrangler/deployed-version.test.ts +150 -0
- package/src/wrangler/deployed-version.ts +130 -0
- package/src/wrangler/live-version.ts +363 -0
- package/src/wrangler/stage-secrets.ts +146 -0
|
@@ -103,13 +103,13 @@ describe('publish workflow definition', () => {
|
|
|
103
103
|
|
|
104
104
|
it('a private forge release with cross-built Apple targets publishes from one job', () => {
|
|
105
105
|
const rendered = renderPublishWorkflowYaml({
|
|
106
|
-
repoName: '
|
|
106
|
+
repoName: 'acme/app',
|
|
107
107
|
actionsProvider: 'forgejo',
|
|
108
108
|
runsOn: ['nixos-latest-x64', 'self-hosted'],
|
|
109
109
|
platformTargetGlobs: ['*-macos', '*-linux'],
|
|
110
110
|
macosPlatformArchitectures: ['arm64'],
|
|
111
111
|
platformProducer: { kind: 'linux-cross', preflight: 'sh scripts/prepare-macos-sdk.sh', env: { AXE_CROSS: '1' } },
|
|
112
|
-
privateNpm: { scope: '@
|
|
112
|
+
privateNpm: { scope: '@acme', readTokenEnv: 'READ_ENV', publishTokenEnv: 'PUBLISH_ENV' },
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
// One job: no producer job to hand outputs over from, so no transfer at all.
|
|
@@ -202,18 +202,18 @@ describe('publish workflow definition', () => {
|
|
|
202
202
|
);
|
|
203
203
|
const finalJob = platform.slice(platform.indexOf(' publish-on-linux:'));
|
|
204
204
|
|
|
205
|
-
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length:
|
|
206
|
-
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length:
|
|
205
|
+
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 17 }, (_, index) => index + 1));
|
|
206
|
+
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 20 }, (_, index) => index + 1));
|
|
207
207
|
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 10 }, (_, index) => index + 1));
|
|
208
208
|
expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1));
|
|
209
|
-
expect(singleJob).toContain('# Step
|
|
210
|
-
expect(singleJob).toContain('# Step
|
|
211
|
-
expect(singleJob).toContain('# Step
|
|
212
|
-
expect(linuxCandidate).toContain('# Step
|
|
209
|
+
expect(singleJob).toContain('# Step 15\n - name: 🏷️ Tag release');
|
|
210
|
+
expect(singleJob).toContain('# Step 16\n - name: 📦 Publish release (${{ steps.version.outputs.mode }})');
|
|
211
|
+
expect(singleJob).toContain('# Step 17\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
212
|
+
expect(linuxCandidate).toContain('# Step 11\n - name: 🔒 Capture candidate release SHA');
|
|
213
213
|
expect(linuxCandidate).toContain(
|
|
214
|
-
'# Step
|
|
214
|
+
'# Step 7\n - name: ✅ Check managed monorepo files (${{ steps.plan.outputs.mode }})',
|
|
215
215
|
);
|
|
216
|
-
expect(linuxCandidate).toContain('# Step
|
|
216
|
+
expect(linuxCandidate).toContain('# Step 20\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
217
217
|
expect(macosPlatform).toContain('# Step 6\n - name: 🔢 Version release');
|
|
218
218
|
expect(macosPlatform).toContain('# Step 7\n - name: 🍎 Build selected macOS and iOS release outputs');
|
|
219
219
|
expect(macosPlatform).toContain('# Step 10\n - name: 🧹 Cleanup and cache Nix/devenv');
|
|
@@ -311,14 +311,22 @@ describe('publish workflow definition', () => {
|
|
|
311
311
|
expect(singleJob.match(/- name: 🏷️ Tag release/g)).toHaveLength(1);
|
|
312
312
|
expect(singleJob).toContain(' run: smoo release tag --dry-run "${{ inputs.dry_run }}"');
|
|
313
313
|
expect(singleJob.indexOf('- name: 🧯 Repair pending releases')).toBeLessThan(
|
|
314
|
-
singleJob.indexOf('- name:
|
|
314
|
+
singleJob.indexOf('- name: 🧭 Plan release'),
|
|
315
315
|
);
|
|
316
|
+
// Lint and test hash the source, so they run BEFORE the version commit and
|
|
317
|
+
// reuse the task hashes ci already computed. Build stays after it: an
|
|
318
|
+
// artifact built from pre-bump sources would carry the previous version.
|
|
319
|
+
expect(singleJob.indexOf('- name: 🧭 Plan release')).toBeLessThan(singleJob.indexOf('- name: 🔍 Lint'));
|
|
320
|
+
expect(singleJob.indexOf('- name: 🧪 Unit Tests')).toBeLessThan(singleJob.indexOf('- name: 🔢 Version release'));
|
|
316
321
|
expect(singleJob.indexOf('- name: 🔢 Version release')).toBeLessThan(singleJob.indexOf('- name: 🔨 Build'));
|
|
317
|
-
expect(singleJob.indexOf('- name:
|
|
322
|
+
expect(singleJob.indexOf('- name: 🔨 Build')).toBeLessThan(singleJob.indexOf('- name: 🏷️ Tag release'));
|
|
323
|
+
// The gates select from the plan, not from a version step that has not run.
|
|
324
|
+
expect(singleJob).toContain('--targets lint --projects "${{ steps.plan.outputs.projects }}"');
|
|
325
|
+
expect(singleJob).toContain("if: steps.plan.outputs.mode != 'none'");
|
|
318
326
|
expect(singleJob.indexOf('- name: 🏷️ Tag release')).toBeLessThan(singleJob.indexOf('- name: 📦 Publish release'));
|
|
319
327
|
// The Release section starts at tagging now, not at publishing.
|
|
320
328
|
expect(singleJob).toContain(
|
|
321
|
-
'# --- Release ------------------------------------------------------------\n\n # Step
|
|
329
|
+
'# --- Release ------------------------------------------------------------\n\n # Step 15\n - name: 🏷️ Tag release',
|
|
322
330
|
);
|
|
323
331
|
});
|
|
324
332
|
|
|
@@ -919,6 +927,8 @@ class WorkflowScenarioState {
|
|
|
919
927
|
tests: [],
|
|
920
928
|
validates: 0,
|
|
921
929
|
};
|
|
930
|
+
planned = false;
|
|
931
|
+
gatesBeforeVersion: { lints: string[]; tests: string[] } = { lints: [], tests: [] };
|
|
922
932
|
|
|
923
933
|
constructor(private readonly config: WorkflowScenarioConfig) {
|
|
924
934
|
for (const gap of [...config.repairs, ...config.current]) {
|
|
@@ -960,8 +970,16 @@ class WorkflowScenarioState {
|
|
|
960
970
|
this.repaired.add(gap.tag);
|
|
961
971
|
}
|
|
962
972
|
},
|
|
973
|
+
planRelease: async ({ bump }) => {
|
|
974
|
+
// The plan runs before anything is written, and the gates that follow
|
|
975
|
+
// it must therefore have seen no version commit yet.
|
|
976
|
+
this.planned = true;
|
|
977
|
+
expect(bump).toBe(this.config.bump);
|
|
978
|
+
return this.config.version;
|
|
979
|
+
},
|
|
963
980
|
versionRelease: async ({ bump, dryRun }) => {
|
|
964
981
|
this.versionObservedNxVersionActions = this.nxVersionActions;
|
|
982
|
+
this.gatesBeforeVersion = { lints: [...this.validationState.lints], tests: [...this.validationState.tests] };
|
|
965
983
|
expect(bump).toBe(this.config.bump);
|
|
966
984
|
expect(dryRun).toBe(this.config.dryRun);
|
|
967
985
|
return this.config.version;
|
|
@@ -1271,8 +1289,8 @@ it('keeps job-local step anchors contiguous once Cargo credentials add a setup s
|
|
|
1271
1289
|
|
|
1272
1290
|
// Each job gains exactly one step over the credential-free counts, and the
|
|
1273
1291
|
// hand-numbered platform renderers must renumber with it.
|
|
1274
|
-
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length:
|
|
1275
|
-
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length:
|
|
1292
|
+
expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 18 }, (_, index) => index + 1));
|
|
1293
|
+
expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 21 }, (_, index) => index + 1));
|
|
1276
1294
|
expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 11 }, (_, index) => index + 1));
|
|
1277
1295
|
expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 15 }, (_, index) => index + 1));
|
|
1278
1296
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
cargoCrossTestArchiveFile,
|
|
5
|
+
cargoCrossTestArchiveTargetName,
|
|
6
|
+
cargoCrossTestTargetName,
|
|
7
|
+
} from '@smoothbricks/nx-plugin/cross-check-policy';
|
|
3
8
|
import { PRODUCTION_PUSH_DEPLOY_TAG } from '../lib/deploy-tags.js';
|
|
4
9
|
import type {
|
|
5
10
|
NonEmptyArray,
|
|
@@ -24,6 +29,11 @@ export enum CiWorkflowStepKind {
|
|
|
24
29
|
BrowserTests = 'browser-tests',
|
|
25
30
|
Lint = 'lint',
|
|
26
31
|
UnitTests = 'unit-tests',
|
|
32
|
+
CrossToolchainPreflight = 'cross-toolchain-preflight',
|
|
33
|
+
CrossTestArchives = 'cross-test-archives',
|
|
34
|
+
UploadCrossTestArchives = 'upload-cross-test-archives',
|
|
35
|
+
DownloadCrossTestArchives = 'download-cross-test-archives',
|
|
36
|
+
CrossTests = 'cross-tests',
|
|
27
37
|
ManagedFilesCheck = 'managed-files-check',
|
|
28
38
|
ManagedFilesDispatch = 'managed-files-dispatch',
|
|
29
39
|
Deploy = 'deploy',
|
|
@@ -51,6 +61,23 @@ export interface DeployStepSecretConfig {
|
|
|
51
61
|
deploySecrets?: Record<string, string>;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
/**
|
|
65
|
+
* One foreign target triple's test archive: what the Linux job cross-builds and
|
|
66
|
+
* a native runner executes.
|
|
67
|
+
*
|
|
68
|
+
* Both fields are DERIVED, never hand-written: the triple comes from the cargo
|
|
69
|
+
* workspace's `[workspace.metadata.smoothbricks.test] cross-targets`, and the
|
|
70
|
+
* path is where the inferred `cargo-cross-test-archive-<triple>` target
|
|
71
|
+
* declares its output. That is why this is not a `smoo.github` key — a triple
|
|
72
|
+
* repeated in CI config could name an archive the graph cannot build, and the
|
|
73
|
+
* workflow would only find out on the runner.
|
|
74
|
+
*/
|
|
75
|
+
export interface CiCrossTestArchive {
|
|
76
|
+
triple: string;
|
|
77
|
+
/** Repository-relative path of the archive file, from the target's output. */
|
|
78
|
+
path: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
export interface CiWorkflowDefinitionOptions extends DeployStepSecretConfig {
|
|
55
82
|
actionsProvider?: PackageSmooGithub['actionsProvider'];
|
|
56
83
|
deploy: boolean;
|
|
@@ -92,6 +119,41 @@ export interface CiWorkflowDefinitionOptions extends DeployStepSecretConfig {
|
|
|
92
119
|
e2eSecrets?: Record<string, string>;
|
|
93
120
|
/** Emit the production-on-push job (some project carries PRODUCTION_PUSH_DEPLOY_TAG). */
|
|
94
121
|
productionOnPush?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Cross-built test archives, one per foreign target triple the cargo
|
|
124
|
+
* workspace declares. The Linux job builds and uploads each one; a second job
|
|
125
|
+
* downloads the `*-apple-darwin` ones and EXECUTES them on `macosRunsOn`,
|
|
126
|
+
* which is how a cross compile gets proved by running rather than by linking.
|
|
127
|
+
*
|
|
128
|
+
* Empty or absent renders exactly what a repository without cross archives
|
|
129
|
+
* renders — no step, no job, no numbering shift.
|
|
130
|
+
*/
|
|
131
|
+
crossTestArchives?: readonly CiCrossTestArchive[];
|
|
132
|
+
/**
|
|
133
|
+
* macOS runner labels for the darwin execution job
|
|
134
|
+
* (`smoo.github.macosRunsOn`, the same labels the publish workflow's macOS
|
|
135
|
+
* legs use). Default macos-latest.
|
|
136
|
+
*
|
|
137
|
+
* A self-hosted label here is one specific machine. That machine being asleep
|
|
138
|
+
* does not skip the job: it queues, and the run FAILS on the job's
|
|
139
|
+
* `timeout-minutes`. That is the trade this opt-in buys — a red run when the
|
|
140
|
+
* Mac is unreachable, in exchange for the darwin binaries being tested by
|
|
141
|
+
* execution at all. `continue-on-error` would buy the green back by making
|
|
142
|
+
* the job unable to report anything, which is worse than not having it.
|
|
143
|
+
*/
|
|
144
|
+
macosRunsOn?: string | string[];
|
|
145
|
+
/**
|
|
146
|
+
* Declared Linux cross-platform producer (`smoo.github.platformProducer`):
|
|
147
|
+
* the toolchain preflight and the environment a foreign-target build needs on
|
|
148
|
+
* a Linux runner. The publish workflow already renders this declaration for
|
|
149
|
+
* its cross artifact job, so the cross test archive reuses it rather than
|
|
150
|
+
* carrying a second copy of one repository's SDK and linker setup.
|
|
151
|
+
*
|
|
152
|
+
* Rendered STEP-scoped here, never as job env: this environment turns the
|
|
153
|
+
* repository's native producers into cross producers, and Validate's other
|
|
154
|
+
* steps are host builds that must stay host builds.
|
|
155
|
+
*/
|
|
156
|
+
platformProducer?: PackageSmooGithub['platformProducer'];
|
|
95
157
|
}
|
|
96
158
|
|
|
97
159
|
type CiWorkflowStepInput = Omit<CiWorkflowStep, 'number'>;
|
|
@@ -117,6 +179,18 @@ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkfl
|
|
|
117
179
|
{ kind: CiWorkflowStepKind.Lint, name: '🔍 Lint' },
|
|
118
180
|
{ kind: CiWorkflowStepKind.UnitTests, name: '🧪 Unit Tests' },
|
|
119
181
|
);
|
|
182
|
+
if (options.crossTestArchives?.length) {
|
|
183
|
+
if (options.platformProducer !== undefined) {
|
|
184
|
+
steps.push({
|
|
185
|
+
kind: CiWorkflowStepKind.CrossToolchainPreflight,
|
|
186
|
+
name: 'Check cross-platform toolchain prerequisites',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
steps.push(
|
|
190
|
+
{ kind: CiWorkflowStepKind.CrossTestArchives, name: '🎯 Build cross-target test archives' },
|
|
191
|
+
{ kind: CiWorkflowStepKind.UploadCrossTestArchives, name: '📤 Upload cross-target test archives' },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
120
194
|
if (options.browserTests) {
|
|
121
195
|
steps.push({ kind: CiWorkflowStepKind.BrowserTests, name: '🌐 Browser Tests' });
|
|
122
196
|
}
|
|
@@ -135,6 +209,7 @@ export function renderCiWorkflowYaml(options: CiWorkflowDefinitionOptions): stri
|
|
|
135
209
|
return [
|
|
136
210
|
renderCiWorkflowHeader(options),
|
|
137
211
|
renderCiWorkflowSteps(steps, options),
|
|
212
|
+
renderCrossTestExecutionJob(options),
|
|
138
213
|
renderE2eDeploymentJob(options),
|
|
139
214
|
renderProductionDeployJob(options),
|
|
140
215
|
].join('');
|
|
@@ -156,12 +231,21 @@ permissions:
|
|
|
156
231
|
${options.deploy ? ' deployments: write\n' : ''} statuses: write
|
|
157
232
|
|
|
158
233
|
concurrency:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#
|
|
162
|
-
# canceling
|
|
234
|
+
${
|
|
235
|
+
options.deploy
|
|
236
|
+
? ` # One in-flight run per ref. Pushes to the staging push branch queue behind a
|
|
237
|
+
# running workflow instead of canceling it, so a newer push never cancels the
|
|
238
|
+
# deploy job mid-flight. Pull requests and other branches keep canceling
|
|
239
|
+
# superseded runs.
|
|
163
240
|
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
164
|
-
cancel-in-progress: \${{ github.ref != ${stagingRefLiteral(options)} }}
|
|
241
|
+
cancel-in-progress: \${{ github.ref != ${stagingRefLiteral(options)} }}`
|
|
242
|
+
: ` # This workflow validates and never deploys, so there is no in-flight
|
|
243
|
+
# deployment for a newer push to protect: every ref cancels its superseded
|
|
244
|
+
# runs. Queuing them instead serializes the staging branch, and a burst of
|
|
245
|
+
# pushes then reports the newest commit one full run per queued push late.
|
|
246
|
+
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
247
|
+
cancel-in-progress: true`
|
|
248
|
+
}
|
|
165
249
|
|
|
166
250
|
defaults:
|
|
167
251
|
run:
|
|
@@ -312,6 +396,61 @@ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOpt
|
|
|
312
396
|
return nxSmartStep(step, 'lint', 'Lint');
|
|
313
397
|
case CiWorkflowStepKind.UnitTests:
|
|
314
398
|
return nxSmartStep(step, 'test', 'Unit Tests');
|
|
399
|
+
case CiWorkflowStepKind.CrossToolchainPreflight:
|
|
400
|
+
// The repository's own preflight, the same one the publish workflow's
|
|
401
|
+
// cross artifact job runs — an SDK to provision, a linker to check.
|
|
402
|
+
// `working-directory: .` because this workflow's steps default to the
|
|
403
|
+
// direnv shell directory while a preflight is repository-relative.
|
|
404
|
+
return [
|
|
405
|
+
` - name: ${step.name}`,
|
|
406
|
+
' working-directory: .',
|
|
407
|
+
...platformProducerStepEnvLines(options),
|
|
408
|
+
' run: |',
|
|
409
|
+
' set -euo pipefail',
|
|
410
|
+
...crossToolchainPreflight(options).map((line) => ` ${line}`),
|
|
411
|
+
];
|
|
412
|
+
case CiWorkflowStepKind.CrossTestArchives:
|
|
413
|
+
// Cross-COMPILES the workspace's test binaries for each declared foreign
|
|
414
|
+
// triple and packs them; runs nothing. `nx-run-many` rather than
|
|
415
|
+
// `nx-smart`, because an affected-graph selection would skip the archive
|
|
416
|
+
// on a commit that changed nothing Rust — and the sibling job that
|
|
417
|
+
// executes it has no way to build one.
|
|
418
|
+
return [
|
|
419
|
+
` - name: ${step.name}`,
|
|
420
|
+
...platformProducerStepEnvLines(options),
|
|
421
|
+
` run: smoo github-ci nx-run-many --targets "${crossArchiveTargetNames(options).join(',')}"`,
|
|
422
|
+
];
|
|
423
|
+
case CiWorkflowStepKind.UploadCrossTestArchives:
|
|
424
|
+
// retention-days: 1 — the only consumer is the sibling job in this same
|
|
425
|
+
// run, and these archives are the whole workspace's test binaries.
|
|
426
|
+
return artifactStepLines(options.actionsProvider, step.name, 'upload', [
|
|
427
|
+
`name: ${CROSS_TEST_ARCHIVE_ARTIFACT}`,
|
|
428
|
+
'path: |',
|
|
429
|
+
...crossTestArchives(options).map((archive) => ` ${archive.path}`),
|
|
430
|
+
// error, not ignore: a missing archive means the cross build silently
|
|
431
|
+
// produced nothing, and the execute job would then have nothing to run
|
|
432
|
+
// and no reason to say so.
|
|
433
|
+
'if-no-files-found: error',
|
|
434
|
+
'retention-days: 1',
|
|
435
|
+
]);
|
|
436
|
+
case CiWorkflowStepKind.DownloadCrossTestArchives:
|
|
437
|
+
// Restores each archive at the path its own Nx target declares as output,
|
|
438
|
+
// which is where the runner target reads it from. upload-artifact roots
|
|
439
|
+
// the artifact at the least common ancestor of what it uploaded, so the
|
|
440
|
+
// download path is that shared directory.
|
|
441
|
+
return artifactStepLines(options.actionsProvider, step.name, 'download', [
|
|
442
|
+
`name: ${CROSS_TEST_ARCHIVE_ARTIFACT}`,
|
|
443
|
+
`path: ${crossTestArchiveDirectory(options)}`,
|
|
444
|
+
]);
|
|
445
|
+
case CiWorkflowStepKind.CrossTests:
|
|
446
|
+
// EXECUTES, and that is all it can do: every one of these targets is
|
|
447
|
+
// `nextest run --archive-file`, which extracts prebuilt binaries into its
|
|
448
|
+
// own temporary directory. nextest never invokes cargo on this path
|
|
449
|
+
// (measured), so nothing here can compile even if a toolchain is present.
|
|
450
|
+
return [
|
|
451
|
+
` - name: ${step.name}`,
|
|
452
|
+
` run: smoo github-ci nx-run-many --targets "${darwinCrossTestTargetNames(options).join(',')}"`,
|
|
453
|
+
];
|
|
315
454
|
case CiWorkflowStepKind.ManagedFilesCheck:
|
|
316
455
|
// The authoritative drift check is nearly free here (devenv is already
|
|
317
456
|
// up). --warn never fails the job and publishes the step output
|
|
@@ -944,6 +1083,10 @@ function renderYamlList(values: string[], spaces: number): string {
|
|
|
944
1083
|
* is GitHub's automatic "Set up job"); every preflight the main job runs also
|
|
945
1084
|
* runs here before SetupDevenv, so the middle and cleanup anchors shift with
|
|
946
1085
|
* the configuration instead of baking a fixed step number.
|
|
1086
|
+
*
|
|
1087
|
+
* `middle` is the FIRST of the job's own steps. A job with more than one — the
|
|
1088
|
+
* cross-test job downloads before it runs — passes how many it has, so the
|
|
1089
|
+
* cleanup anchor stays the number of the step GitHub will actually report.
|
|
947
1090
|
*/
|
|
948
1091
|
interface FollowUpStepNumbers {
|
|
949
1092
|
cargo: number | undefined;
|
|
@@ -953,12 +1096,13 @@ interface FollowUpStepNumbers {
|
|
|
953
1096
|
cleanup: number;
|
|
954
1097
|
}
|
|
955
1098
|
|
|
956
|
-
function followUpStepNumbers(options: CiWorkflowDefinitionOptions): FollowUpStepNumbers {
|
|
1099
|
+
function followUpStepNumbers(options: CiWorkflowDefinitionOptions, middleSteps = 1): FollowUpStepNumbers {
|
|
957
1100
|
let number = 3;
|
|
958
1101
|
const cargo = options.cargoCredentials !== undefined ? number++ : undefined;
|
|
959
1102
|
const sources = options.sourceCheckouts?.length ? number++ : undefined;
|
|
960
1103
|
const setup = number++;
|
|
961
|
-
const middle = number
|
|
1104
|
+
const middle = number;
|
|
1105
|
+
number += middleSteps;
|
|
962
1106
|
const cleanup = number++;
|
|
963
1107
|
return { cargo, sources, setup, middle, cleanup };
|
|
964
1108
|
}
|
|
@@ -985,6 +1129,146 @@ function followUpCleanupStep(numbers: FollowUpStepNumbers): CiWorkflowStep[] {
|
|
|
985
1129
|
return [{ kind: CiWorkflowStepKind.SaveNixDevenv, name: '🧹 Cleanup and cache Nix/devenv', number: numbers.cleanup }];
|
|
986
1130
|
}
|
|
987
1131
|
|
|
1132
|
+
/**
|
|
1133
|
+
* How long the darwin execution job may take, download included. Generous
|
|
1134
|
+
* because the archive is the whole workspace's test binaries, and bounded
|
|
1135
|
+
* because the runner behind `macosRunsOn` may be one person's laptop: an
|
|
1136
|
+
* unreachable runner queues, and this number is when the run says so.
|
|
1137
|
+
*/
|
|
1138
|
+
const CROSS_TEST_JOB_TIMEOUT_MINUTES = 30;
|
|
1139
|
+
|
|
1140
|
+
/** Rust names every Apple-desktop triple this way; iOS and simulator do not. */
|
|
1141
|
+
const DARWIN_TRIPLE_SUFFIX = '-apple-darwin';
|
|
1142
|
+
|
|
1143
|
+
/** One artifact carrying every triple's archive, scoped to the run that built it. */
|
|
1144
|
+
const CROSS_TEST_ARCHIVE_ARTIFACT = `cross-test-archives-${githubExpression('github.run_id')}`;
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Executes the cross-built test binaries on a machine where they are native.
|
|
1148
|
+
*
|
|
1149
|
+
* This job COMPILES NOTHING, and cannot: its only Rust input is the archive the
|
|
1150
|
+
* Validate job uploaded, and `nextest run --archive-file` extracts prebuilt
|
|
1151
|
+
* binaries rather than invoking cargo (measured: zero cargo invocations). It
|
|
1152
|
+
* needs the checkout for the workspace `--workspace-remap .` points at and for
|
|
1153
|
+
* Nx to resolve the target, and setup-devenv for `nx` and `cargo-nextest`
|
|
1154
|
+
* themselves — the repository's ordinary shell, not a cross toolchain, no SDK,
|
|
1155
|
+
* no `rustup target add`.
|
|
1156
|
+
*
|
|
1157
|
+
* `needs: main` and a success gate rather than `!cancelled()`: an archive that
|
|
1158
|
+
* was never uploaded cannot be executed, and a job that starts anyway would
|
|
1159
|
+
* fail on a missing artifact and report it as a test failure.
|
|
1160
|
+
*
|
|
1161
|
+
* Only `*-apple-darwin` archives run here, because macOS runner labels are the
|
|
1162
|
+
* only native runner this declaration names. A repository declaring some other
|
|
1163
|
+
* foreign triple still gets its archive BUILT — which proves that triple
|
|
1164
|
+
* compiles — and no execution job, which is the honest rendering of having
|
|
1165
|
+
* nowhere to run it.
|
|
1166
|
+
*/
|
|
1167
|
+
function renderCrossTestExecutionJob(options: CiWorkflowDefinitionOptions): string {
|
|
1168
|
+
const archives = darwinCrossTestArchives(options);
|
|
1169
|
+
if (archives.length === 0) return '';
|
|
1170
|
+
const numbers = followUpStepNumbers(options, 2);
|
|
1171
|
+
const middleSteps: CiWorkflowStep[] = [
|
|
1172
|
+
{
|
|
1173
|
+
kind: CiWorkflowStepKind.DownloadCrossTestArchives,
|
|
1174
|
+
name: '📥 Download cross-target test archives',
|
|
1175
|
+
number: numbers.middle,
|
|
1176
|
+
},
|
|
1177
|
+
{ kind: CiWorkflowStepKind.CrossTests, name: '🧪 Cross-Target Unit Tests', number: numbers.middle + 1 },
|
|
1178
|
+
];
|
|
1179
|
+
return `
|
|
1180
|
+
macos-cross-tests:
|
|
1181
|
+
name: Unit Tests (${archives.map((archive) => archive.triple).join(', ')})
|
|
1182
|
+
needs: main
|
|
1183
|
+
${options.macosRunsOn === undefined ? ' runs-on: macos-latest' : renderRunsOnLine(options.macosRunsOn)}
|
|
1184
|
+
timeout-minutes: ${CROSS_TEST_JOB_TIMEOUT_MINUTES}
|
|
1185
|
+
if: \${{ needs.main.result == 'success' }}
|
|
1186
|
+
env:
|
|
1187
|
+
GH_TOKEN: \${{ github.token }}
|
|
1188
|
+
${remoteCacheJobEnvLines(options.remoteCache)}${cargoCredentialJobEnvLines(options.cargoCredentials)}${privateNpmReadTokenJobEnv(options)} steps:
|
|
1189
|
+
${renderCiWorkflowSteps(followUpSetupSteps(options, numbers), options)}
|
|
1190
|
+
${renderCiWorkflowSteps(middleSteps, options)}
|
|
1191
|
+
${renderCiWorkflowSteps(followUpCleanupStep(numbers), options)}`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* The declared archives, checked against the naming the Nx graph would have
|
|
1196
|
+
* produced. A path that is not `cargoCrossTestArchiveFile(triple)` under some
|
|
1197
|
+
* project root cannot have come from that target, so it is refused here rather
|
|
1198
|
+
* than uploaded as an artifact nothing on the other side can find.
|
|
1199
|
+
*/
|
|
1200
|
+
function crossTestArchives(options: CiWorkflowDefinitionOptions): CiCrossTestArchive[] {
|
|
1201
|
+
return (options.crossTestArchives ?? []).map((archive) => {
|
|
1202
|
+
const suffix = cargoCrossTestArchiveFile(archive.triple);
|
|
1203
|
+
const path = archive.path.split('\\').join('/');
|
|
1204
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(archive.triple)) {
|
|
1205
|
+
throw new Error(`smoo cross test archive triple must be a target triple, got ${JSON.stringify(archive.triple)}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (path !== suffix && !path.endsWith(`/${suffix}`)) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`smoo cross test archive for ${archive.triple} must be a repository-relative ${suffix}, got ${archive.path}`,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
if (path.startsWith('/') || path.split('/').includes('..')) {
|
|
1213
|
+
throw new Error(`smoo cross test archive path must stay inside the repository, got ${archive.path}`);
|
|
1214
|
+
}
|
|
1215
|
+
return { triple: archive.triple, path };
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function crossArchiveTargetNames(options: CiWorkflowDefinitionOptions): string[] {
|
|
1220
|
+
return crossTestArchives(options).map((archive) => cargoCrossTestArchiveTargetName(archive.triple));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The declared preflight, as lines. A declaration with nothing in it is a
|
|
1225
|
+
* mechanism error, and it surfaces here at render time rather than as a step
|
|
1226
|
+
* whose `run:` block is empty.
|
|
1227
|
+
*/
|
|
1228
|
+
function crossToolchainPreflight(options: CiWorkflowDefinitionOptions): string[] {
|
|
1229
|
+
const preflight = options.platformProducer?.preflight ?? '';
|
|
1230
|
+
if (preflight.trim().length === 0) {
|
|
1231
|
+
throw new Error('smoo.github.platformProducer requires a nonempty toolchain preflight command.');
|
|
1232
|
+
}
|
|
1233
|
+
return preflight.split('\n');
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** The producer environment, step-scoped, quoted exactly as the publish workflow quotes it. */
|
|
1237
|
+
function platformProducerStepEnvLines(options: CiWorkflowDefinitionOptions): string[] {
|
|
1238
|
+
const entries = Object.entries(options.platformProducer?.env ?? {});
|
|
1239
|
+
if (entries.length === 0) return [];
|
|
1240
|
+
return [' env:', ...entries.map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`)];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function darwinCrossTestArchives(options: CiWorkflowDefinitionOptions): CiCrossTestArchive[] {
|
|
1244
|
+
return crossTestArchives(options).filter((archive) => archive.triple.endsWith(DARWIN_TRIPLE_SUFFIX));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function darwinCrossTestTargetNames(options: CiWorkflowDefinitionOptions): string[] {
|
|
1248
|
+
return darwinCrossTestArchives(options).map((archive) => cargoCrossTestTargetName(archive.triple));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Where the download lands. upload-artifact roots an artifact at the least
|
|
1253
|
+
* common ancestor of the files it uploaded, so every archive in one artifact
|
|
1254
|
+
* has to share a parent directory for the restored layout to be the one the
|
|
1255
|
+
* runner targets read — true for one cargo workspace, false the moment a
|
|
1256
|
+
* second one also declares cross targets, which is refused here instead of
|
|
1257
|
+
* silently restoring the archives one directory too high.
|
|
1258
|
+
*/
|
|
1259
|
+
function crossTestArchiveDirectory(options: CiWorkflowDefinitionOptions): string {
|
|
1260
|
+
const directories = new Set(
|
|
1261
|
+
darwinCrossTestArchives(options).map((archive) => archive.path.slice(0, archive.path.lastIndexOf('/') + 1) || './'),
|
|
1262
|
+
);
|
|
1263
|
+
if (directories.size > 1) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`smoo cross test archives must share one directory to travel as one artifact, got ${[...directories].join(', ')}`,
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const [directory] = [...directories];
|
|
1269
|
+
return (directory ?? './').replace(/\/$/, '');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
988
1272
|
function renderE2eDeploymentJob(options: CiWorkflowDefinitionOptions): string {
|
|
989
1273
|
if (!options.e2eDeployment) return '';
|
|
990
1274
|
const numbers = followUpStepNumbers(options);
|
|
@@ -1025,6 +1309,7 @@ function renderProductionDeployJob(options: CiWorkflowDefinitionOptions): string
|
|
|
1025
1309
|
needs: ${needs}
|
|
1026
1310
|
${renderRunsOnLine(options.runsOn)}
|
|
1027
1311
|
timeout-minutes: 30
|
|
1312
|
+
# prettier-ignore
|
|
1028
1313
|
if: \${{ !cancelled() && github.event_name == 'push' && github.ref == ${stagingRefLiteral(options)} && needs.main.result == 'success'${e2eGate} }}
|
|
1029
1314
|
${environmentLine(options.environments?.production)} env:
|
|
1030
1315
|
GH_TOKEN: \${{ github.token }}
|
|
@@ -1032,7 +1317,8 @@ ${remoteCacheJobEnvLines(options.remoteCache)}${cargoCredentialJobEnvLines(optio
|
|
|
1032
1317
|
${renderCiWorkflowSteps(followUpSetupSteps(options, numbers), options)}
|
|
1033
1318
|
# Step ${numbers.middle}
|
|
1034
1319
|
- name: 🚀 Deploy Production
|
|
1035
|
-
${renderOptionalLines(deployStepSecretEnvLines(options))}
|
|
1320
|
+
${renderOptionalLines(deployStepSecretEnvLines(options))} # prettier-ignore
|
|
1321
|
+
run: smoo github-ci nx-deploy --stage production --mode run-many --select-tag ${PRODUCTION_PUSH_DEPLOY_TAG} --name "Deploy Production" --step ${numbers.middle}
|
|
1036
1322
|
|
|
1037
1323
|
${renderCiWorkflowSteps(followUpCleanupStep(numbers), options)}`;
|
|
1038
1324
|
}
|
|
@@ -8,6 +8,7 @@ import fc from 'fast-check';
|
|
|
8
8
|
import { type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
9
9
|
import {
|
|
10
10
|
anyProjectHasTagForTest,
|
|
11
|
+
crossTestArchivesForTest,
|
|
11
12
|
DEVENV_MODULE_IMPORT,
|
|
12
13
|
deployTargetInfoFromProjects,
|
|
13
14
|
extractInlineLocalBlocksForTest,
|
|
@@ -205,6 +206,31 @@ describe('managed publish platform discovery', () => {
|
|
|
205
206
|
expect(macosPlatformArchitecturesForTest(['simulator-arm64-ios', 'cli-arm64-macos'])).toEqual(['arm64']);
|
|
206
207
|
expect(macosPlatformArchitecturesForTest(['build', 'cli-x64-linux', 'test'])).toEqual([]);
|
|
207
208
|
});
|
|
209
|
+
|
|
210
|
+
it('reads cross-built archives out of the graph, at each project root', () => {
|
|
211
|
+
expect(
|
|
212
|
+
crossTestArchivesForTest({
|
|
213
|
+
'@acme/codebase': {
|
|
214
|
+
root: '.',
|
|
215
|
+
targets: { 'cargo-cross-test-archive-aarch64-apple-darwin': {}, 'cargo-test-archive': {}, test: {} },
|
|
216
|
+
},
|
|
217
|
+
'acme-embedded': {
|
|
218
|
+
root: 'packages/embedded',
|
|
219
|
+
targets: { 'cargo-cross-test-archive-thumbv7em-none-eabihf': {} },
|
|
220
|
+
},
|
|
221
|
+
'acme-web': { root: 'packages/web', targets: { build: {}, 'cargo-cross-test-aarch64-apple-darwin': {} } },
|
|
222
|
+
}),
|
|
223
|
+
).toEqual([
|
|
224
|
+
{ triple: 'aarch64-apple-darwin', path: 'target/nextest/archive-aarch64-apple-darwin.tar.zst' },
|
|
225
|
+
{
|
|
226
|
+
triple: 'thumbv7em-none-eabihf',
|
|
227
|
+
path: 'packages/embedded/target/nextest/archive-thumbv7em-none-eabihf.tar.zst',
|
|
228
|
+
},
|
|
229
|
+
]);
|
|
230
|
+
// A repository with no cross declaration has no such target, so the CI
|
|
231
|
+
// renderer gets an empty list and renders the workflow it renders today.
|
|
232
|
+
expect(crossTestArchivesForTest({ '@acme/codebase': { root: '.', targets: { test: {} } } })).toEqual([]);
|
|
233
|
+
});
|
|
208
234
|
});
|
|
209
235
|
|
|
210
236
|
describe('managed devenv module import', () => {
|
|
@@ -461,6 +487,7 @@ const context = (overrides: Partial<ManagedFileContext>): ManagedFileContext =>
|
|
|
461
487
|
platformTargetGlobs: [],
|
|
462
488
|
releasePlatformTargetGlobs: [],
|
|
463
489
|
macosPlatformArchitectures: [],
|
|
490
|
+
crossTestArchives: [],
|
|
464
491
|
...overrides,
|
|
465
492
|
});
|
|
466
493
|
|
|
@@ -578,7 +605,7 @@ describe('release platform families', () => {
|
|
|
578
605
|
it('excluding every Apple family renders the single-job Linux publish shape', () => {
|
|
579
606
|
const globs = releasePlatformTargetGlobsFor(['*-macos', '*-linux'], ['*-macos']);
|
|
580
607
|
const rendered = renderPublishWorkflowYaml({
|
|
581
|
-
repoName: '
|
|
608
|
+
repoName: 'acme/app',
|
|
582
609
|
platformTargetGlobs: globs,
|
|
583
610
|
macosPlatformArchitectures: [],
|
|
584
611
|
runsOn: ['nixos-latest-x64', 'self-hosted'],
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
cargoCrossTestArchiveFile,
|
|
6
|
+
cargoCrossTestTripleFromArchiveTarget,
|
|
7
|
+
} from '@smoothbricks/nx-plugin/cross-check-policy';
|
|
4
8
|
import { MACOS_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
5
9
|
import { isStageDerivedDeploy, PRODUCTION_PUSH_DEPLOY_TAG } from '../lib/deploy-tags.js';
|
|
6
10
|
import {
|
|
@@ -17,9 +21,15 @@ import {
|
|
|
17
21
|
readValidatedPackageJson,
|
|
18
22
|
} from '../lib/json.js';
|
|
19
23
|
import { listReleasePackages, packageInfo } from '../lib/workspace.js';
|
|
20
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
loadNxProjects,
|
|
26
|
+
type NxProjects,
|
|
27
|
+
projectRootFromNxProjectJson,
|
|
28
|
+
targetNamesFromNxProjectJson,
|
|
29
|
+
targetNamesFromProjects,
|
|
30
|
+
} from '../nx/index.js';
|
|
21
31
|
import { resolvePrivateNpmWorkflowConfig } from '../release/private-npm.js';
|
|
22
|
-
import { privateNpmReadTokenJobEnv, renderCiWorkflowYaml } from './ci-workflow.js';
|
|
32
|
+
import { type CiCrossTestArchive, privateNpmReadTokenJobEnv, renderCiWorkflowYaml } from './ci-workflow.js';
|
|
23
33
|
import { renderRunsOnLine } from './github-runs-on.js';
|
|
24
34
|
import { renderPrPreviewCleanupWorkflowYaml } from './pr-preview-cleanup-workflow.js';
|
|
25
35
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
@@ -181,6 +191,12 @@ export interface ManagedFileContext {
|
|
|
181
191
|
cargoCredentials?: PackageCargoCredentialsConfig;
|
|
182
192
|
/** Declared self-hosted Nx remote cache from the root smoo config; absent means local caching only. */
|
|
183
193
|
remoteCache?: PackageRemoteCacheConfig;
|
|
194
|
+
/**
|
|
195
|
+
* Cross-built test archives the graph can produce, one per foreign target
|
|
196
|
+
* triple the cargo workspace declares. Empty for a repository that declares
|
|
197
|
+
* none, which renders a CI workflow with no cross steps and no macOS job.
|
|
198
|
+
*/
|
|
199
|
+
crossTestArchives: CiCrossTestArchive[];
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
interface DeployTargetInfo {
|
|
@@ -407,6 +423,9 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
|
|
|
407
423
|
deploySecrets: context.ciDeploySecrets,
|
|
408
424
|
e2eSecrets: context.ciE2eSecrets,
|
|
409
425
|
productionOnPush: context.hasProductionPushDeployTargets,
|
|
426
|
+
crossTestArchives: context.crossTestArchives,
|
|
427
|
+
macosRunsOn: context.macosRunsOn,
|
|
428
|
+
platformProducer: context.platformProducer,
|
|
410
429
|
});
|
|
411
430
|
}
|
|
412
431
|
if (file.source === 'publish-workflow') {
|
|
@@ -503,6 +522,7 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
|
|
|
503
522
|
? macosPlatformArchitecturesForTest(targetNames)
|
|
504
523
|
: [],
|
|
505
524
|
privateNpm,
|
|
525
|
+
crossTestArchives: crossTestArchivesForTest(nxProjects),
|
|
506
526
|
};
|
|
507
527
|
}
|
|
508
528
|
|
|
@@ -510,6 +530,40 @@ export function hasExactTargetForTest(targetNames: Iterable<string>, target: str
|
|
|
510
530
|
return [...targetNames].includes(target);
|
|
511
531
|
}
|
|
512
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Test seam: the cross-built test archives this graph can produce, read back
|
|
535
|
+
* out of the inferred `cargo-cross-test-archive-<triple>` targets.
|
|
536
|
+
*
|
|
537
|
+
* Derived, not configured. The triple is declared exactly once — in the cargo
|
|
538
|
+
* workspace's own `[workspace.metadata.smoothbricks.test] cross-targets`, where
|
|
539
|
+
* a Rust target triple belongs — and both halves of the workflow are rendered
|
|
540
|
+
* from the targets that declaration produced. A second copy in `smoo.github`
|
|
541
|
+
* could name a triple the graph cannot build, and CI would discover that only
|
|
542
|
+
* on the runner.
|
|
543
|
+
*
|
|
544
|
+
* The path is the target's own output location, so the artifact the Linux job
|
|
545
|
+
* uploads and the file the macOS job's runner opens are the same string
|
|
546
|
+
* computed once.
|
|
547
|
+
*/
|
|
548
|
+
export function crossTestArchivesForTest(projects: NxProjects): CiCrossTestArchive[] {
|
|
549
|
+
const archives: CiCrossTestArchive[] = [];
|
|
550
|
+
for (const project of Object.values(projects)) {
|
|
551
|
+
const projectRoot = projectRootFromNxProjectJson(project);
|
|
552
|
+
for (const targetName of targetNamesFromNxProjectJson(project)) {
|
|
553
|
+
const triple = cargoCrossTestTripleFromArchiveTarget(targetName);
|
|
554
|
+
if (triple === null) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const file = cargoCrossTestArchiveFile(triple);
|
|
558
|
+
archives.push({
|
|
559
|
+
triple,
|
|
560
|
+
path: projectRoot === undefined || projectRoot === '.' ? file : `${projectRoot}/${file}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return archives.sort((left, right) => left.triple.localeCompare(right.triple));
|
|
565
|
+
}
|
|
566
|
+
|
|
513
567
|
/**
|
|
514
568
|
* The platform families the release workflow produces: the graph's families
|
|
515
569
|
* minus the ones the repository excludes. Excluding a family the graph does not
|
|
@@ -63,7 +63,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
63
63
|
const rootPackage = await readJson(join(root, 'package.json'));
|
|
64
64
|
const nxJson = await readJson(join(root, 'nx.json'));
|
|
65
65
|
expect(rootPackage.scripts).toEqual({
|
|
66
|
-
'check:linux': 'tooling/devenv
|
|
66
|
+
'check:linux': 'tooling/devenv -P linux-cross shell -- nx run-many -t cargo-lint-cross',
|
|
67
67
|
clean: 'nx run-many -t clean; nx reset',
|
|
68
68
|
'clean:node_modules':
|
|
69
69
|
'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
|