@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.
Files changed (60) hide show
  1. package/README.md +45 -10
  2. package/dist/cli.js +10 -7
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +6 -20
  5. package/dist/lib/deploy-tags.d.ts +17 -3
  6. package/dist/lib/deploy-tags.d.ts.map +1 -1
  7. package/dist/lib/deploy-tags.js +23 -4
  8. package/dist/monorepo/ci-workflow.d.ts +56 -0
  9. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  10. package/dist/monorepo/ci-workflow.js +199 -2
  11. package/dist/monorepo/managed-files.d.ts +28 -2
  12. package/dist/monorepo/managed-files.d.ts.map +1 -1
  13. package/dist/monorepo/managed-files.js +74 -22
  14. package/dist/release/github-release.d.ts +10 -0
  15. package/dist/release/github-release.d.ts.map +1 -1
  16. package/dist/release/github-release.js +11 -5
  17. package/dist/release/index.d.ts.map +1 -1
  18. package/dist/release/index.js +4 -5
  19. package/dist/secrets/commands.d.ts +52 -13
  20. package/dist/secrets/commands.d.ts.map +1 -1
  21. package/dist/secrets/commands.js +220 -34
  22. package/dist/secrets/index.d.ts +21 -1
  23. package/dist/secrets/index.d.ts.map +1 -1
  24. package/dist/secrets/index.js +110 -4
  25. package/dist/secrets/repository.d.ts +60 -0
  26. package/dist/secrets/repository.d.ts.map +1 -0
  27. package/dist/secrets/repository.js +145 -0
  28. package/dist/wrangler/cloudflare.d.ts +7 -0
  29. package/dist/wrangler/cloudflare.d.ts.map +1 -1
  30. package/dist/wrangler/cloudflare.js +10 -0
  31. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  32. package/dist/wrangler/deploy-stage.js +50 -24
  33. package/dist/wrangler/stage-secrets.d.ts +57 -0
  34. package/dist/wrangler/stage-secrets.d.ts.map +1 -0
  35. package/dist/wrangler/stage-secrets.js +178 -0
  36. package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
  37. package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
  38. package/package.json +2 -2
  39. package/src/cli.ts +33 -10
  40. package/src/github-ci/index.test.ts +6 -7
  41. package/src/github-ci/index.ts +11 -19
  42. package/src/lib/deploy-tags.ts +22 -4
  43. package/src/monorepo/__tests__/ci-workflow.test.ts +161 -0
  44. package/src/monorepo/ci-workflow.ts +277 -2
  45. package/src/monorepo/managed-files.test.ts +96 -37
  46. package/src/monorepo/managed-files.ts +94 -23
  47. package/src/monorepo/package-policy.test.ts +1 -1
  48. package/src/release/__tests__/github-release.test.ts +8 -4
  49. package/src/release/github-release.ts +13 -5
  50. package/src/release/index.ts +4 -4
  51. package/src/secrets/commands.test.ts +28 -0
  52. package/src/secrets/commands.ts +237 -35
  53. package/src/secrets/index.test.ts +118 -1
  54. package/src/secrets/index.ts +108 -4
  55. package/src/secrets/repository.test.ts +98 -0
  56. package/src/secrets/repository.ts +164 -0
  57. package/src/wrangler/cloudflare.ts +18 -0
  58. package/src/wrangler/deploy-stage.test.ts +258 -11
  59. package/src/wrangler/deploy-stage.ts +70 -38
  60. 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`. Nx caches that target on the Cargo inputs, so a hit is a
355
- prior real Linux clippy and never enters linux-cross. A miss falls through to `bun run check:linux`, which is
356
- `tooling/devenv --quiet -P linux-cross shell --` around that same Nx target and writes the cache. `git push --no-verify`
357
- skips the hook.
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 be
428
- stage-derived — carry `stage-deploy-target` or deploy through `smoo wrangler deploy-stage` otherwise `--select-tag`
429
- finds nothing and the job logs `No run-many deploy projects; skipping production.`
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 every declared `deploy` target `cache: false`. An Nx cache hit on a deploy means "we
448
- once uploaded this hash", which a rollback silently falsifies — the workspace is unchanged, so the hash is unchanged,
449
- so a cached deploy would report success while the previous version keeps serving.
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 the repository lacks')
431
- .option('--repo <owner/name>', 'repository to read secrets from; defaults to the current checkout')
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 <name>')
437
- .description('Set one repository secret from a pasted value; the value is read without echo and never logged')
438
- .option('--repo <owner/name>', 'repository to set the secret on')
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>', 'repository to set the secrets on')
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;AAmBD,KAAK,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAKpD,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,GAAG,UAAU,EAC7B,aAAa,CAAC,EAAE,MAAM,EACtB,KAAK,CAAC,EAAE,MAAM,EACd,YAAY,UAAQ,GACnB,MAAM,EAAE,CAaV;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,oBAAoB,CAmD9G;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CA0C/E;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhF;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBxF;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkB9G;AAED,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAGf;AA8CD,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,CACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,GAAG,UAAU,EAC7B,KAAK,EAAE,eAAe,EACtB,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,YAAY,CAAC,EAAE,yBAAyB,CAAC;IACzC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,uBAAuB,EAChC,YAAY,GAAE,4BAAiC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAmFf;AAsCD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,WAAW,EAAE,MAAM,CAAC,UAAU,EAC9B,KAAK,EAAE,yBAAyB,GAAG,SAAS,EAC5C,iBAAiB,EAAE,MAAM,GACxB,eAAe,CAsBjB;AAkBD,8GAA8G;AAC9G,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAShH;AA2BD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GACjD,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAClF;AAED,qBAAa,0BAA2B,YAAW,sBAAsB;IACvE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAqB/E;CACF;AAED,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,KAAK,MAAM,EAAE,EAC1B,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,CAAC,UAAU,EACrC,MAAM,GAAE,sBAAyD,GAChE,OAAO,CAAC,IAAI,CAAC,CAyEf;AAwDD,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,CAAC,UAAU,EAC9B,KAAK,EAAE,yBAAyB,GAAG,SAAS,GAC3C,OAAO,CAUT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/github-ci/index.ts"],"names":[],"mappings":"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"}
@@ -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 { isStageDerivedDeploy, PERMANENT_DEPLOY_TAG, STAGING_DEPLOY_TAG } from '../lib/deploy-tags.js';
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 _ip0 = input => true;
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 _ip0 = input => true;
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 deploy = definition.targets?.deploy;
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.includes(requireTag))
431
+ if (requireTag && !tags?.includes(requireTag))
446
432
  continue;
447
- if (isStageDerived || (stage === 'staging' && isStagingOnly)) {
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 a deploy target is stage-derived (the tag, or the `smoo wrangler deploy-stage` command) rather than a set of
9
- * per-stage configurations; CI selection and workflow generation must agree on this.
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 isStageDerivedDeploy(tags: string[] | undefined, command: string | undefined): boolean;
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":"AAMA,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAE9D,uEAAuE;AACvE,eAAO,MAAM,0BAA0B,kCAAkC,CAAC;AAE1E;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAErG"}
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"}
@@ -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 a deploy target is stage-derived (the tag, or the `smoo wrangler deploy-stage` command) rather than a set of
13
- * per-stage configurations; CI selection and workflow generation must agree on this.
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 isStageDerivedDeploy(tags, command) {
16
- return tags?.includes(STAGE_DEPLOY_TAG) === true || command?.includes('smoo wrangler deploy-stage') === true;
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":"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"}
1
+ {"version":3,"file":"ci-workflow.d.ts","sourceRoot":"","sources":["../../src/monorepo/ci-workflow.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,aAAa,EACb,6BAA6B,EAE7B,uBAAuB,EACvB,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,2BAA2B,EAC5B,MAAM,gBAAgB,CAAC;AAGxB,oBAAY,kBAAkB;IAC5B,QAAQ,aAAa;IACrB,eAAe,qBAAqB;IACpC,WAAW,iBAAiB;IAC5B,gBAAgB,sBAAsB;IACtC,SAAS,gBAAgB;IACzB,cAAc,qBAAqB;IACnC,KAAK,UAAU;IACf,YAAY,kBAAkB;IAC9B,IAAI,SAAS;IACb,SAAS,eAAe;IACxB,uBAAuB,8BAA8B;IACrD,iBAAiB,wBAAwB;IACzC,uBAAuB,+BAA+B;IACtD,yBAAyB,iCAAiC;IAC1D,UAAU,gBAAgB;IAC1B,iBAAiB,wBAAwB;IACzC,oBAAoB,2BAA2B;IAC/C,MAAM,WAAW;IACjB,WAAW,kBAAkB;IAC7B,cAAc,qBAAqB;IACnC,aAAa,oBAAoB;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,cAAc,CAAC,EAAE,YAAY,CAAC;IAC9B,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,2EAA2E;IAC3E,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAC;IACrC;;;;OAIG;IACH,eAAe,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAChD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,6BAA6B,CAAC;IACjD;;;;OAIG;IACH,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,yGAAyG;IACzG,YAAY,CAAC,EAAE,6BAA6B,CAAC;IAC7C,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAClD;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAChC;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;CAC1D;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,EAAE,CA4CvF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,MAAM,CASjF;AAkED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,IAAI,CAAC,2BAA2B,EAAE,YAAY,CAAC,GAAG,MAAM,CAM1G;AAiPD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,SAAS,2BAA2B,EAAE,GAChD,MAAM,EAAE,CA+BV;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,2BAA2B,GAAG,2BAA2B,CA8BxG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,SAAS,GAAG,MAAM,CAS3F;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,wBAAwB,GAAG,wBAAwB,CAgB/F;AA+BD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,6BAA6B,GAAG,SAAS,GAAG,MAAM,CASpG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,6BAA6B,GAAG,MAAM,EAAE,CAyG9G;AAqCD,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,6BAA6B,GAAG,6BAA6B,CAkC9G;AAqGD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,UAIjC,CAAC;AAiCF;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAMjF;AAiRD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,EAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GAAG,UAAU,EAC3B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,SAAS,SAAc,GACtB,MAAM,EAAE,CAmBV"}
@@ -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 '';