@smoothbricks/cli 0.11.18 → 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 CHANGED
@@ -430,11 +430,28 @@ access makes cleanup refuse before it deletes anything rather than half-clean a
430
430
  A wrong value type anywhere in `smoo.github` fails `smoo monorepo update` and `smoo github-ci nx-deploy` with the
431
431
  offending path, rather than silently falling back to the defaults.
432
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
+
433
450
  Tag a project `production-push-deploy-target` to have it deployed to production by a generated `deploy-production` job
434
451
  that runs after Validate and the e2e job succeed on a push to the staging push branch
435
- (`smoo github-ci nx-deploy --stage production --select-tag production-push-deploy-target`). The project must also be
436
- stage-derived — carry `stage-deploy-target` or deploy through `smoo wrangler deploy-stage` otherwise `--select-tag`
437
- 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.`
438
455
 
439
456
  #### Ordering one deploy after another
440
457
 
@@ -452,9 +469,9 @@ drop.
452
469
 
453
470
  What makes the edge safe is that a deploy is never cached and always cheap when there is nothing to do:
454
471
 
455
- - `@smoothbricks/nx-plugin` gives every declared `deploy` target `cache: false`. An Nx cache hit on a deploy means "we
456
- once uploaded this hash", which a rollback silently falsifies — the workspace is unchanged, so the hash is unchanged,
457
- 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.
458
475
  - `smoo wrangler deploy-stage` reads live state first. If the version tagged with this task's hash is already the one
459
476
  serving 100% of traffic, it returns `remote-cache-hit` after two API calls: no upload, no migration, no traffic shift.
460
477
  - The expensive, purely file-derived half belongs in a sibling `deploy-build` target (build the artifact, register it,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/github-ci/index.ts"],"names":[],"mappings":"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"}
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
  }
@@ -1,5 +1,6 @@
1
1
  import { type NonEmptyArray, type PackageCargoCredentialsConfig, type PackagePrivateNpmConfig, type PackageRemoteCacheConfig, type PackageSmooGithub, type PackageSmooGithubEnvironments, type PackageSourceCheckoutConfig } from '../lib/json.js';
2
2
  import { type NxProjects } from '../nx/index.js';
3
+ import type { DeploymentStage } from '../wrangler/stage.js';
3
4
  import { type CiCrossTestArchive } from './ci-workflow.js';
4
5
  /**
5
6
  * Repos may append their own content to a managed file below this marker —
@@ -97,6 +98,8 @@ export declare const managedFileTargetsForTest: {
97
98
  target: string;
98
99
  executable: boolean | undefined;
99
100
  }[];
101
+ /** Test seam: the managed targets a repository of this shape gets, in file order. */
102
+ export declare function managedFileTargetsForContext(context: ManagedFileContext): string[];
100
103
  export declare function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]>;
101
104
  /** Renders a generated workflow through its real managed-file descriptor, so the context wiring is covered. */
102
105
  export declare function renderManagedWorkflowForTest(source: 'ci-workflow' | 'publish-workflow', context: ManagedFileContext): string;
@@ -134,8 +137,8 @@ export declare function platformTargetGlobsForTest(targetNames: Iterable<string>
134
137
  * platform builds serialize no matter how many workers Nx is given.
135
138
  */
136
139
  export declare function macosPlatformArchitecturesForTest(targetNames: Iterable<string>): string[];
137
- /** Test seam: deploy configuration presence/provider from graph nodes. */
138
- export declare function deployTargetInfoFromProjects(projects: NxProjects, configuration: string): DeployTargetInfo;
140
+ /** Test seam: what the stage flow deploys, and with which provider, from graph nodes. */
141
+ export declare function deployTargetInfoFromProjects(projects: NxProjects, stage: DeploymentStage): DeployTargetInfo;
139
142
  /** Test seam: whether any project in the graph carries the Nx tag. */
140
143
  export declare function anyProjectHasTagForTest(projects: NxProjects, tag: string): boolean;
141
144
  export declare function printResults(results: FileResult[]): void;
@@ -1 +1 @@
1
- {"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,6BAA6B,EAElC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,iBAAiB,EACtB,KAAK,6BAA6B,EAClC,KAAK,2BAA2B,EAEjC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,KAAK,kBAAkB,EAAmD,MAAM,kBAAkB,CAAC;AAO5G;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,yEAAyE,CAAC;AAW3G,sFAAsF;AACtF,iBAAS,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAIlF;AAED,uCAAuC;AACvC,eAAO,MAAM,wBAAwB,0BAAoB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;8BAE8B;AAC9B,iBAAS,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkCxG;AAED,wCAAwC;AACxC,eAAO,MAAM,+BAA+B,iCAA2B,CAAC;AAExE;+EAC+E;AAC/E,iBAAS,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAuBtF;AAED,yCAAyC;AACzC,eAAO,MAAM,gCAAgC,kCAA4B,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;CACxG;AAED,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,uBAAuB,EAAE,OAAO,CAAC;IACjC,0BAA0B,EAAE,OAAO,CAAC;IACpC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,uBAAuB,EAAE,OAAO,CAAC;IACjC,8BAA8B,EAAE,OAAO,CAAC;IACxC,qBAAqB,CAAC,EAAE,YAAY,CAAC;IACrC,wBAAwB,CAAC,EAAE,YAAY,CAAC;IACxC,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,yFAAyF;IACzF,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;IACzD,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,cAAc,CAAC,EAAE,6BAA6B,CAAC;IAC/C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,qFAAqF;IACrF,0BAA0B,EAAE,MAAM,EAAE,CAAC;IACrC,0BAA0B,EAAE,MAAM,EAAE,CAAC;IACrC,wFAAwF;IACxF,UAAU,CAAC,EAAE,uBAAuB,CAAC;IACrC,sFAAsF;IACtF,eAAe,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAChD,kGAAkG;IAClG,gBAAgB,CAAC,EAAE,6BAA6B,CAAC;IACjD,uGAAuG;IACvG,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC;;;;OAIG;IACH,iBAAiB,EAAE,kBAAkB,EAAE,CAAC;CACzC;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB;AAwID,eAAO,MAAM,yBAAyB;;;GAAyE,CAAC;AAIhH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAG9G;AA6CD,+GAA+G;AAC/G,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,aAAa,GAAG,kBAAkB,EAC1C,OAAO,EAAE,kBAAkB,GAC1B,MAAM,CAMR;AA8HD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,UAAU,GAAG,kBAAkB,EAAE,CAiBnF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,mBAAmB,EAAE,SAAS,MAAM,EAAE,EACtC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACtC,MAAM,EAAE,CAIV;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAMlF;AAED;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAezF;AAED,0EAA0E;AAC1E,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,gBAAgB,CAY1G;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAElF;AAqED,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQxE;AAED,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAY/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBxE"}
1
+ {"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { cargoCrossTestArchiveFile, cargoCrossTestTripleFromArchiveTarget, } from '@smoothbricks/nx-plugin/cross-check-policy';
5
5
  import { MACOS_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
6
- import { isStageDerivedDeploy, PRODUCTION_PUSH_DEPLOY_TAG } from '../lib/deploy-tags.js';
6
+ import { PRODUCTION_PUSH_DEPLOY_TAG, stageDeploysProject } from '../lib/deploy-tags.js';
7
7
  import { ciPushBranches, readValidatedPackageJson, } from '../lib/json.js';
8
8
  import { listReleasePackages, packageInfo } from '../lib/workspace.js';
9
9
  import { loadNxProjects, projectRootFromNxProjectJson, targetNamesFromNxProjectJson, targetNamesFromProjects, } from '../nx/index.js';
@@ -236,15 +236,27 @@ const managedFiles = [
236
236
  ];
237
237
  export const managedFileTargetsForTest = managedFiles.map(({ target, executable }) => ({ target, executable }));
238
238
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
239
+ /**
240
+ * Whether a repository of this shape has the file at all. A repository that owns
241
+ * no packages has nothing to publish, and one the stage flow does not deploy to
242
+ * Cloudflare has no preview stage to clean up.
243
+ */
244
+ function managedFileApplies(file, context) {
245
+ if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
246
+ return false;
247
+ }
248
+ return file.cloudflareDeployOnly !== true || context.stagingDeployProvider === 'cloudflare';
249
+ }
250
+ /** Test seam: the managed targets a repository of this shape gets, in file order. */
251
+ export function managedFileTargetsForContext(context) {
252
+ return managedFiles.filter((file) => managedFileApplies(file, context)).map((file) => file.target);
253
+ }
239
254
  export async function applyManagedFiles(root, mode) {
240
255
  const context = await getManagedFileContext(root);
241
256
  return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
242
257
  }
243
258
  function applyManagedFile(root, file, mode, context) {
244
- if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
245
- return { target: file.target, action: 'skipped' };
246
- }
247
- if (file.cloudflareDeployOnly === true && context.stagingDeployProvider !== 'cloudflare') {
259
+ if (!managedFileApplies(file, context)) {
248
260
  return { target: file.target, action: 'skipped' };
249
261
  }
250
262
  const target = resolve(root, file.target);
@@ -483,12 +495,12 @@ export function macosPlatformArchitecturesForTest(targetNames) {
483
495
  }
484
496
  return [...architectures].sort((left, right) => left.localeCompare(right));
485
497
  }
486
- /** Test seam: deploy configuration presence/provider from graph nodes. */
487
- export function deployTargetInfoFromProjects(projects, configuration) {
498
+ /** Test seam: what the stage flow deploys, and with which provider, from graph nodes. */
499
+ export function deployTargetInfoFromProjects(projects, stage) {
488
500
  let exists = false;
489
501
  let provider;
490
502
  for (const project of Object.values(projects)) {
491
- const info = deployTargetInfoFromProject(project, configuration);
503
+ const info = deployTargetInfoFromProject(project, stage);
492
504
  if (!info.exists) {
493
505
  continue;
494
506
  }
@@ -501,22 +513,24 @@ export function deployTargetInfoFromProjects(projects, configuration) {
501
513
  export function anyProjectHasTagForTest(projects, tag) {
502
514
  return Object.values(projects).some((project) => project.tags?.includes(tag));
503
515
  }
504
- function deployTargetInfoFromProject(project, configuration) {
516
+ /**
517
+ * A generated deploy job exists for the projects CI would actually deploy, which
518
+ * is `stageDeploysProject` and nothing else — the same rule `smoo github-ci
519
+ * nx-deploy` selects by, so the job and its selection cannot disagree. Reading
520
+ * the target's presence instead would render a deploy job, with cloud
521
+ * credentials, into a repository whose only wrangler manifests document a
522
+ * Durable Object binding for a published library's consumers.
523
+ *
524
+ * The command is read for the PROVIDER only: which cloud CLI runs says which
525
+ * credentials the job needs, and says nothing about whether CI runs it.
526
+ */
527
+ function deployTargetInfoFromProject(project, stage) {
505
528
  const deploy = project.targets?.deploy;
506
- if (!deploy) {
507
- return { exists: false };
508
- }
509
- const baseCommandValue = deploy.options?.command ?? deploy.command;
510
- const baseCommand = typeof baseCommandValue === 'string' ? baseCommandValue : '';
511
- if (isStageDerivedDeploy(project.tags, baseCommand)) {
512
- return { exists: true, provider: deployProvider(baseCommand) };
513
- }
514
- const config = deploy.configurations?.[configuration];
515
- if (!config) {
529
+ if (!deploy || !stageDeploysProject(project.tags, stage)) {
516
530
  return { exists: false };
517
531
  }
518
- const commandValue = config.command ?? config.options?.command ?? baseCommand;
519
- return { exists: true, provider: deployProvider(typeof commandValue === 'string' ? commandValue : '') };
532
+ const command = deploy.options?.command ?? deploy.command;
533
+ return { exists: true, provider: deployProvider(typeof command === 'string' ? command : '') };
520
534
  }
521
535
  function deployProvider(command) {
522
536
  return command.includes('wrangler ') ? 'cloudflare' : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.11.18",
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.12",
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",
@@ -847,8 +847,11 @@ describe('event-aware stage deployment', () => {
847
847
  ).toThrow(/same-repository/);
848
848
  });
849
849
 
850
- it('selects stage-derived deploy targets plus staging-only infrastructure on staging', async () => {
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-derived rule', async () => {
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 website on every stage once it is stage-derived', async () => {
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
  });
@@ -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 {
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?: unknown;
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 deploy = definition.targets?.deploy;
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.includes(requireTag)) continue;
582
- if (isStageDerived || (stage === 'staging' && isStagingOnly)) {
573
+ if (requireTag && !tags?.includes(requireTag)) continue;
574
+ if (stageDeploysProject(tags, stage)) {
583
575
  selected.push(project);
584
576
  }
585
577
  }
@@ -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 a deploy target is stage-derived (the tag, or the `smoo wrangler deploy-stage` command) rather than a set of
18
- * per-stage configurations; CI selection and workflow generation must agree on this.
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 isStageDerivedDeploy(tags: string[] | undefined, command: string | undefined): boolean {
21
- return tags?.includes(STAGE_DEPLOY_TAG) === true || command?.includes('smoo wrangler deploy-stage') === true;
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
  }
@@ -18,6 +18,7 @@ import {
18
18
  LOCAL_SECTION_MARKER,
19
19
  type ManagedFileContext,
20
20
  macosPlatformArchitecturesForTest,
21
+ managedFileTargetsForContext,
21
22
  managedFileTargetsForTest,
22
23
  platformTargetGlobsForTest,
23
24
  reinsertInlineLocalBlocksForTest,
@@ -263,14 +264,10 @@ describe('nx graph project helpers', () => {
263
264
  },
264
265
  },
265
266
  app: {
267
+ // What the plugin infers for a private wrangler project, and what a published library
268
+ // holding a wrangler manifest used to get: a deploy target no tag enrols in CI.
266
269
  targets: {
267
- deploy: {
268
- options: { command: 'echo no' },
269
- configurations: {
270
- staging: { command: 'wrangler deploy --env staging' },
271
- production: { options: { command: 'wrangler deploy --env production' } },
272
- },
273
- },
270
+ deploy: { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } },
274
271
  'package-macos': {},
275
272
  },
276
273
  },
@@ -296,43 +293,40 @@ describe('nx graph project helpers', () => {
296
293
  expect(hasExactTargetForTest(nearMisses, 'e2e-deployment')).toBe(false);
297
294
  });
298
295
 
299
- it('detects deploy configurations and cloudflare provider from graph nodes', () => {
300
- expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({
301
- exists: true,
302
- provider: 'cloudflare',
303
- });
304
- expect(deployTargetInfoFromProjects(sampleProjects, 'production')).toEqual({
305
- exists: true,
306
- provider: 'cloudflare',
307
- });
308
- expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
296
+ it('deploys nothing for a wrangler deploy target no tag names', () => {
297
+ expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({ exists: false });
298
+ expect(deployTargetInfoFromProjects(sampleProjects, 'production')).toEqual({ exists: false });
309
299
  });
310
300
 
311
- it('recognizes convention-driven deploy-stage targets without per-stage configurations', () => {
312
- const projects: NxProjects = {
313
- app: {
314
- targets: {
315
- deploy: {
316
- options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' },
317
- },
318
- },
319
- },
320
- };
301
+ it('reads the tags as precedence: permanent excludes, staging narrows, stage generalizes', () => {
302
+ const deploy = { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } };
303
+ const stageFor = (tags: string[]) => ({
304
+ staging: deployTargetInfoFromProjects({ site: { tags, targets: { deploy } } }, 'staging').exists,
305
+ production: deployTargetInfoFromProjects({ site: { tags, targets: { deploy } } }, 'production').exists,
306
+ });
321
307
 
322
- expect(deployTargetInfoFromProjects(projects, 'staging')).toEqual({ exists: true, provider: 'cloudflare' });
323
- expect(deployTargetInfoFromProjects(projects, 'production')).toEqual({ exists: true, provider: 'cloudflare' });
308
+ expect(stageFor(['stage-deploy-target'])).toEqual({ staging: true, production: true });
309
+ expect(stageFor(['staging-deploy-target'])).toEqual({ staging: true, production: false });
310
+ expect(stageFor(['permanent-deploy-target'])).toEqual({ staging: false, production: false });
311
+ // Both spellings on one project is a contradiction; the exclusion wins, which is what
312
+ // `nx show projects --exclude=tag:permanent-deploy-target` does to it in CI too.
313
+ expect(stageFor(['permanent-deploy-target', 'stage-deploy-target'])).toEqual({
314
+ staging: false,
315
+ production: false,
316
+ });
317
+ expect(stageFor(['npm:private'])).toEqual({ staging: false, production: false });
324
318
  });
325
319
 
326
- it('treats a stage-deploy-target tag as a deploy target for every stage, like the deploy-stage command', () => {
327
- const projects: NxProjects = {
328
- site: {
329
- tags: ['stage-deploy-target'],
330
- targets: { deploy: { command: 'wrangler deploy --config dist/wrangler.json' } },
331
- },
320
+ it('reads the command for the provider only: which credentials the generated job needs', () => {
321
+ const cloudflare: NxProjects = {
322
+ site: { tags: ['stage-deploy-target'], targets: { deploy: { command: 'wrangler deploy --config dist/w.json' } } },
323
+ };
324
+ const elsewhere: NxProjects = {
325
+ site: { tags: ['stage-deploy-target'], targets: { deploy: { command: 'bun tooling/deploy-site.ts' } } },
332
326
  };
333
327
 
334
- expect(deployTargetInfoFromProjects(projects, 'staging')).toEqual({ exists: true, provider: 'cloudflare' });
335
- expect(deployTargetInfoFromProjects(projects, 'production')).toEqual({ exists: true, provider: 'cloudflare' });
328
+ expect(deployTargetInfoFromProjects(cloudflare, 'staging')).toEqual({ exists: true, provider: 'cloudflare' });
329
+ expect(deployTargetInfoFromProjects(elsewhere, 'staging')).toEqual({ exists: true, provider: undefined });
336
330
  });
337
331
 
338
332
  it('finds a tag carried by any project in the graph', () => {
@@ -582,6 +576,44 @@ describe('CI workflow rendering by repo shape', () => {
582
576
  expect(rendered).toContain(' deploy-production:');
583
577
  });
584
578
 
579
+ it('renders no deploy job at all for a repo whose wrangler projects carry no deploy tag', () => {
580
+ // The regression this exists for: published libraries ship a wrangler manifest to document a
581
+ // Durable Object binding for their consumers, the plugin inferred a deploy target from each,
582
+ // and a repository that deploys nothing from CI grew a Deploy Stage step holding Cloudflare
583
+ // credentials, a deployments permission, and a preview-cleanup workflow.
584
+ const deploy = { options: { command: 'smoo wrangler deploy-stage --stage {args.stage}' } };
585
+ const projects: NxProjects = {
586
+ 'acme-cloudflare': { tags: ['npm:private'], targets: { deploy } },
587
+ 'acme-documents': { tags: ['npm:public'], targets: { deploy } },
588
+ // A private worker, deployed by hand from a developer's shell and never by CI.
589
+ 'acme-service': { targets: { deploy } },
590
+ };
591
+ const staging = deployTargetInfoFromProjects(projects, 'staging');
592
+ const production = deployTargetInfoFromProjects(projects, 'production');
593
+ const shape = context({
594
+ hasStagingDeployTargets: staging.exists,
595
+ stagingDeployProvider: staging.provider,
596
+ hasProductionDeployTargets: production.exists,
597
+ productionDeployProvider: production.provider,
598
+ hasE2eDeploymentTargets: true,
599
+ });
600
+ const ci = renderManagedWorkflowForTest('ci-workflow', shape);
601
+
602
+ expect(ci).not.toContain('Deploy Stage');
603
+ expect(ci).not.toContain('deployments: write');
604
+ expect(ci).not.toContain('CLOUDFLARE_API_TOKEN');
605
+ expect(ci).not.toContain('e2e-deployment');
606
+ expect(renderManagedWorkflowForTest('publish-workflow', shape)).not.toContain('Deploy production');
607
+ expect(managedFileTargetsForContext(shape)).not.toContain('.github/workflows/pr-preview-cleanup.yml');
608
+ });
609
+
610
+ it('adds the preview cleanup workflow only where a cloudflare stage deploy makes preview stages', () => {
611
+ expect(managedFileTargetsForContext(deploying)).toContain('.github/workflows/pr-preview-cleanup.yml');
612
+ expect(managedFileTargetsForContext({ ...deploying, stagingDeployProvider: undefined })).not.toContain(
613
+ '.github/workflows/pr-preview-cleanup.yml',
614
+ );
615
+ });
616
+
585
617
  it('threads environments and secrets from the context into the workflow', () => {
586
618
  const rendered = renderManagedWorkflowForTest('ci-workflow', {
587
619
  ...deploying,
@@ -6,7 +6,7 @@ import {
6
6
  cargoCrossTestTripleFromArchiveTarget,
7
7
  } from '@smoothbricks/nx-plugin/cross-check-policy';
8
8
  import { MACOS_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
9
- import { isStageDerivedDeploy, PRODUCTION_PUSH_DEPLOY_TAG } from '../lib/deploy-tags.js';
9
+ import { PRODUCTION_PUSH_DEPLOY_TAG, stageDeploysProject } from '../lib/deploy-tags.js';
10
10
  import {
11
11
  ciPushBranches,
12
12
  type NonEmptyArray,
@@ -29,6 +29,7 @@ import {
29
29
  targetNamesFromProjects,
30
30
  } from '../nx/index.js';
31
31
  import { resolvePrivateNpmWorkflowConfig } from '../release/private-npm.js';
32
+ import type { DeploymentStage } from '../wrangler/stage.js';
32
33
  import { type CiCrossTestArchive, privateNpmReadTokenJobEnv, renderCiWorkflowYaml } from './ci-workflow.js';
33
34
  import { renderRunsOnLine } from './github-runs-on.js';
34
35
  import { renderPrPreviewCleanupWorkflowYaml } from './pr-preview-cleanup-workflow.js';
@@ -342,6 +343,23 @@ export const managedFileTargetsForTest = managedFiles.map(({ target, executable
342
343
 
343
344
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
344
345
 
346
+ /**
347
+ * Whether a repository of this shape has the file at all. A repository that owns
348
+ * no packages has nothing to publish, and one the stage flow does not deploy to
349
+ * Cloudflare has no preview stage to clean up.
350
+ */
351
+ function managedFileApplies(file: ManagedFile, context: ManagedFileContext): boolean {
352
+ if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
353
+ return false;
354
+ }
355
+ return file.cloudflareDeployOnly !== true || context.stagingDeployProvider === 'cloudflare';
356
+ }
357
+
358
+ /** Test seam: the managed targets a repository of this shape gets, in file order. */
359
+ export function managedFileTargetsForContext(context: ManagedFileContext): string[] {
360
+ return managedFiles.filter((file) => managedFileApplies(file, context)).map((file) => file.target);
361
+ }
362
+
345
363
  export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
346
364
  const context = await getManagedFileContext(root);
347
365
  return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
@@ -353,10 +371,7 @@ function applyManagedFile(
353
371
  mode: 'update' | 'check' | 'diff',
354
372
  context: ManagedFileContext,
355
373
  ): FileResult {
356
- if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
357
- return { target: file.target, action: 'skipped' };
358
- }
359
- if (file.cloudflareDeployOnly === true && context.stagingDeployProvider !== 'cloudflare') {
374
+ if (!managedFileApplies(file, context)) {
360
375
  return { target: file.target, action: 'skipped' };
361
376
  }
362
377
  const target = resolve(root, file.target);
@@ -612,12 +627,12 @@ export function macosPlatformArchitecturesForTest(targetNames: Iterable<string>)
612
627
  return [...architectures].sort((left, right) => left.localeCompare(right));
613
628
  }
614
629
 
615
- /** Test seam: deploy configuration presence/provider from graph nodes. */
616
- export function deployTargetInfoFromProjects(projects: NxProjects, configuration: string): DeployTargetInfo {
630
+ /** Test seam: what the stage flow deploys, and with which provider, from graph nodes. */
631
+ export function deployTargetInfoFromProjects(projects: NxProjects, stage: DeploymentStage): DeployTargetInfo {
617
632
  let exists = false;
618
633
  let provider: DeployTargetInfo['provider'];
619
634
  for (const project of Object.values(projects)) {
620
- const info = deployTargetInfoFromProject(project, configuration);
635
+ const info = deployTargetInfoFromProject(project, stage);
621
636
  if (!info.exists) {
622
637
  continue;
623
638
  }
@@ -632,22 +647,24 @@ export function anyProjectHasTagForTest(projects: NxProjects, tag: string): bool
632
647
  return Object.values(projects).some((project) => project.tags?.includes(tag));
633
648
  }
634
649
 
635
- function deployTargetInfoFromProject(project: NxProjectJson, configuration: string): DeployTargetInfo {
650
+ /**
651
+ * A generated deploy job exists for the projects CI would actually deploy, which
652
+ * is `stageDeploysProject` and nothing else — the same rule `smoo github-ci
653
+ * nx-deploy` selects by, so the job and its selection cannot disagree. Reading
654
+ * the target's presence instead would render a deploy job, with cloud
655
+ * credentials, into a repository whose only wrangler manifests document a
656
+ * Durable Object binding for a published library's consumers.
657
+ *
658
+ * The command is read for the PROVIDER only: which cloud CLI runs says which
659
+ * credentials the job needs, and says nothing about whether CI runs it.
660
+ */
661
+ function deployTargetInfoFromProject(project: NxProjectJson, stage: DeploymentStage): DeployTargetInfo {
636
662
  const deploy = project.targets?.deploy;
637
- if (!deploy) {
638
- return { exists: false };
639
- }
640
- const baseCommandValue = deploy.options?.command ?? deploy.command;
641
- const baseCommand = typeof baseCommandValue === 'string' ? baseCommandValue : '';
642
- if (isStageDerivedDeploy(project.tags, baseCommand)) {
643
- return { exists: true, provider: deployProvider(baseCommand) };
644
- }
645
- const config = deploy.configurations?.[configuration];
646
- if (!config) {
663
+ if (!deploy || !stageDeploysProject(project.tags, stage)) {
647
664
  return { exists: false };
648
665
  }
649
- const commandValue = config.command ?? config.options?.command ?? baseCommand;
650
- return { exists: true, provider: deployProvider(typeof commandValue === 'string' ? commandValue : '') };
666
+ const command = deploy.options?.command ?? deploy.command;
667
+ return { exists: true, provider: deployProvider(typeof command === 'string' ? command : '') };
651
668
  }
652
669
 
653
670
  function deployProvider(command: string): DeployTargetInfo['provider'] {