@smoothbricks/cli 0.11.15 → 0.11.17

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 (70) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +44 -0
  4. package/dist/github-ci/index.d.ts +1 -1
  5. package/dist/github-ci/index.d.ts.map +1 -1
  6. package/dist/github-ci/index.js +7 -10
  7. package/dist/lib/json.d.ts +9 -0
  8. package/dist/lib/json.d.ts.map +1 -1
  9. package/dist/lib/json.js +65 -53
  10. package/dist/lib/secret-names.d.ts +35 -0
  11. package/dist/lib/secret-names.d.ts.map +1 -0
  12. package/dist/lib/secret-names.js +61 -0
  13. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  14. package/dist/monorepo/ci-workflow.js +15 -6
  15. package/dist/monorepo/managed-files.d.ts +9 -0
  16. package/dist/monorepo/managed-files.d.ts.map +1 -1
  17. package/dist/monorepo/managed-files.js +28 -2
  18. package/dist/monorepo/publish-workflow.d.ts +10 -1
  19. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  20. package/dist/monorepo/publish-workflow.js +110 -22
  21. package/dist/release/index.d.ts +1 -1
  22. package/dist/release/index.d.ts.map +1 -1
  23. package/dist/release/index.js +26 -4
  24. package/dist/release/private-npm.d.ts +5 -0
  25. package/dist/release/private-npm.d.ts.map +1 -1
  26. package/dist/release/private-npm.js +26 -5
  27. package/dist/secrets/commands.d.ts +45 -0
  28. package/dist/secrets/commands.d.ts.map +1 -0
  29. package/dist/secrets/commands.js +189 -0
  30. package/dist/secrets/index.d.ts +83 -0
  31. package/dist/secrets/index.d.ts.map +1 -0
  32. package/dist/secrets/index.js +149 -0
  33. package/dist/wrangler/deploy-stage.d.ts +13 -2
  34. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  35. package/dist/wrangler/deploy-stage.js +39 -54
  36. package/dist/wrangler/deployed-version.d.ts +44 -0
  37. package/dist/wrangler/deployed-version.d.ts.map +1 -0
  38. package/dist/wrangler/deployed-version.js +87 -0
  39. package/dist/wrangler/live-version.d.ts +146 -0
  40. package/dist/wrangler/live-version.d.ts.map +1 -0
  41. package/dist/wrangler/live-version.js +326 -0
  42. package/managed/raw/tooling/direnv/devenv.smoo.nix +4 -2
  43. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +4 -1
  44. package/managed/raw/tooling/direnv/toolchain-stamp.ts +88 -0
  45. package/managed/templates/github/actions/setup-devenv/action.yml +1 -1
  46. package/package.json +2 -2
  47. package/src/cli.ts +46 -1
  48. package/src/github-ci/index.test.ts +92 -8
  49. package/src/github-ci/index.ts +7 -10
  50. package/src/lib/json.ts +9 -0
  51. package/src/lib/secret-names.ts +70 -0
  52. package/src/monorepo/__tests__/ci-workflow.test.ts +27 -2
  53. package/src/monorepo/__tests__/publish-workflow.test.ts +85 -13
  54. package/src/monorepo/ci-workflow.ts +17 -6
  55. package/src/monorepo/managed-files.test.ts +26 -0
  56. package/src/monorepo/managed-files.ts +36 -2
  57. package/src/monorepo/publish-workflow.ts +135 -21
  58. package/src/monorepo/secret-references.test.ts +1 -1
  59. package/src/release/__tests__/private-npm-status.test.ts +37 -0
  60. package/src/release/__tests__/private-npm-workflow.test.ts +20 -0
  61. package/src/release/index.ts +27 -2
  62. package/src/release/private-npm.ts +30 -5
  63. package/src/secrets/commands.ts +210 -0
  64. package/src/secrets/index.test.ts +92 -0
  65. package/src/secrets/index.ts +183 -0
  66. package/src/wrangler/deploy-stage.test.ts +190 -12
  67. package/src/wrangler/deploy-stage.ts +72 -45
  68. package/src/wrangler/deployed-version.test.ts +150 -0
  69. package/src/wrangler/deployed-version.ts +130 -0
  70. package/src/wrangler/live-version.ts +363 -0
@@ -1,4 +1,5 @@
1
1
  import { type CloudflareClient } from './cloudflare.js';
2
+ import { type VersionEndpointWaitOptions } from './live-version.js';
2
3
  import { type DeploymentStage } from './stage.js';
3
4
  export interface ProcessResult {
4
5
  exitCode: number;
@@ -24,6 +25,10 @@ export interface WranglerCommandDependencies {
24
25
  runner?: ProcessRunner;
25
26
  cloudflare?: CloudflareClient;
26
27
  processEnv?: NodeJS.ProcessEnv;
28
+ /** Bounds and clock for the post-deploy wait; tests drive it without sleeping. */
29
+ wait?: VersionEndpointWaitOptions;
30
+ /** Where a successful deploy records the tag it made live, for `smoo wrangler deployed-version`. */
31
+ liveVersionCacheDirectory?: string;
27
32
  }
28
33
  export interface DeployStageResult {
29
34
  stage: DeploymentStage;
@@ -36,6 +41,12 @@ export interface DeployStageOptions {
36
41
  stage: string;
37
42
  /** A build-generated flat wrangler.json to deploy instead of `./wrangler.toml` (see `prepareFlatConfig`). */
38
43
  config?: string;
44
+ /**
45
+ * A URL, served by this worker, whose trimmed body is the running version tag. When given, the
46
+ * deploy is not done until that URL answers with the tag it just made live — the control plane
47
+ * accepting a traffic shift is not the edge serving it.
48
+ */
49
+ versionEndpoint?: string;
39
50
  }
40
51
  export declare function deployStage(cwd: string, options: DeployStageOptions, dependencies?: WranglerCommandDependencies): Promise<DeployStageResult>;
41
52
  /**
@@ -60,7 +71,7 @@ export interface CleanupResult {
60
71
  }
61
72
  export declare function cleanupPullRequest(cwd: string, prNumber: number, dependencies?: WranglerCommandDependencies): Promise<CleanupResult>;
62
73
  export declare function nxTaskVersionTag(environment: NodeJS.ProcessEnv): string | undefined;
63
- export declare function findVersionIdByTag(value: unknown, tag: string): string | null;
64
- export declare function isFullCurrentDeployment(value: unknown, versionId: string): boolean;
74
+ /** One wrangler invocation whose stdout must be JSON; shared by the deploy and the read-only query. */
75
+ export declare function wranglerJson(runner: ProcessRunner, args: string[], run: ProcessRunOptions): Promise<unknown>;
65
76
  export {};
66
77
  //# sourceMappingURL=deploy-stage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,11 @@ import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
- import typia from 'typia';
6
5
  import { parseJsonFileText } from '../lib/json.js';
7
6
  import { mergeEnv, printCommandOutput } from '../lib/run.js';
8
7
  import { CloudflareRestClient } from './cloudflare.js';
9
8
  import { parseFlatWranglerConfig, planFlatStageResources } from './flat-config.js';
9
+ import { awaitLiveVersion, awaitVersionEndpoint, currentDeploymentVersionId, findVersionIdByTag, liveVersionCacheDirectory, writeCachedLiveVersion, } from './live-version.js';
10
10
  import { parseDevVarsExample } from './prepare-env.js';
11
11
  import { derivePullRequestStageConfig, derivePullRequestWranglerConfig, hasExactStageSegment, isPullRequestStage, parseDeploymentStage, planConfiguredStageResources, planPullRequestBindings, planPullRequestResources, pullRequestStage, } from './stage.js';
12
12
  /** The child environment `options` describe; every runner, test doubles included, spawns with exactly this. */
@@ -30,15 +30,6 @@ export class BunProcessRunner {
30
30
  return { exitCode, stdout, stderr };
31
31
  }
32
32
  }
33
- const isUnknownRecord = (() => {
34
- const _io0 = input => Object.keys(input).every(key => {
35
- const value = input[key];
36
- if (undefined === value)
37
- return true;
38
- return true;
39
- });
40
- return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
41
- })();
42
33
  export async function deployStage(cwd, options, dependencies = {}) {
43
34
  const stage = parseDeploymentStage(options.stage);
44
35
  const processEnv = dependencies.processEnv ?? process.env;
@@ -73,15 +64,23 @@ export async function deployStage(cwd, options, dependencies = {}) {
73
64
  // `reconcileStageResources` nor `versions list` looks at.
74
65
  const run = prepared.envFlag ? { cwd } : { cwd, unsetEnv: ['CLOUDFLARE_ENV'] };
75
66
  const workerName = prepared.plan.workerName;
67
+ const probe = {
68
+ deployments: () => wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run),
69
+ versions: () => wranglerJson(runner, ['versions', 'list', '--name', workerName, '--json'], run),
70
+ };
76
71
  // The tagged-version lookup runs before the migrations: a cache hit means this exact build is
77
72
  // already live, so its migrations ran with it and re-applying them would touch the remote
78
73
  // database for nothing. The activation and upload paths below still migrate first.
74
+ //
75
+ // This reads Cloudflare every time and never the local cache. A cache hit here would mean
76
+ // "we believe this hash is live" — exactly the belief a rollback falsifies — and believing it
77
+ // would skip the deploy that repairs the rollback. Reading live state is what makes a
78
+ // redundant deploy a proven no-op instead of an assumed one, which is in turn what makes a
79
+ // cross-project `dependsOn: ["<other>:deploy"]` edge cheap enough to be the ordering mechanism.
79
80
  let taggedVersionId = null;
80
81
  if (versionTag && workerExists) {
81
- const versions = await wranglerJson(runner, ['versions', 'list', '--name', workerName, '--json'], run);
82
- const deployments = await wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run);
83
- taggedVersionId = findVersionIdByTag(versions, versionTag);
84
- if (taggedVersionId && isFullCurrentDeployment(deployments, taggedVersionId)) {
82
+ taggedVersionId = findVersionIdByTag(await probe.versions(), versionTag);
83
+ if (taggedVersionId && currentDeploymentVersionId(await probe.deployments()) === taggedVersionId) {
85
84
  return { stage, workerName, action: 'remote-cache-hit', versionTag };
86
85
  }
87
86
  }
@@ -101,6 +100,7 @@ export async function deployStage(cwd, options, dependencies = {}) {
101
100
  ...envArgs,
102
101
  '--yes',
103
102
  ], run);
103
+ await confirmVersionIsLive(probe, versionTag, { stage, workerName, accountId }, options, dependencies, cwd);
104
104
  return { stage, workerName, action: 'activated', versionTag };
105
105
  }
106
106
  const deployArgs = ['deploy', '--config', prepared.deployConfigPath, ...envArgs];
@@ -112,6 +112,9 @@ export async function deployStage(cwd, options, dependencies = {}) {
112
112
  deployArgs.push('--secrets-file', temporarySecretsPath);
113
113
  }
114
114
  await wrangler(runner, deployArgs, run);
115
+ if (versionTag) {
116
+ await confirmVersionIsLive(probe, versionTag, { stage, workerName, accountId }, options, dependencies, cwd);
117
+ }
115
118
  return { stage, workerName, action: 'deployed', ...(versionTag ? { versionTag } : {}) };
116
119
  }
117
120
  finally {
@@ -447,48 +450,30 @@ export function nxTaskVersionTag(environment) {
447
450
  }
448
451
  throw new Error('NX_TASK_HASH must be canonical decimal digits or at least 32 hexadecimal characters.');
449
452
  }
450
- export function findVersionIdByTag(value, tag) {
451
- if (Array.isArray(value)) {
452
- for (const entry of value) {
453
- const found = findVersionIdByTag(entry, tag);
454
- if (found)
455
- return found;
456
- }
457
- return null;
458
- }
459
- if (!isUnknownRecord(value))
460
- return null;
461
- const annotations = isUnknownRecord(value.annotations) ? value.annotations : undefined;
462
- const metadata = isUnknownRecord(value.metadata) ? value.metadata : undefined;
463
- const annotationTag = annotations?.['workers/tag'];
464
- const candidateTag = typeof annotationTag === 'string' ? annotationTag : typeof value.tag === 'string' ? value.tag : metadata?.tag;
465
- if (candidateTag === tag) {
466
- if (typeof value.id === 'string')
467
- return value.id;
468
- if (typeof value.version_id === 'string')
469
- return value.version_id;
470
- }
471
- for (const nested of Object.values(value)) {
472
- const found = findVersionIdByTag(nested, tag);
473
- if (found)
474
- return found;
475
- }
476
- return null;
477
- }
478
- export function isFullCurrentDeployment(value, versionId) {
479
- if (Array.isArray(value))
480
- return value.some((entry) => isFullCurrentDeployment(entry, versionId));
481
- if (!isUnknownRecord(value))
482
- return false;
483
- if (Array.isArray(value.versions)) {
484
- return (value.versions.length === 1 &&
485
- value.versions.some((version) => isUnknownRecord(version) &&
486
- (version.version_id === versionId || version.id === versionId) &&
487
- Number(version.percentage) === 100));
453
+ /**
454
+ * Turns "Cloudflare accepted the traffic shift" into "the new version answers".
455
+ *
456
+ * Without this the deploy step goes green while the edge still serves the previous version — the
457
+ * measured gap was about 20 s — so anything ordered after this deploy could call into code that
458
+ * has not shipped yet. The wait is what a `dependsOn: ["<other>:deploy"]` edge actually buys; an
459
+ * edge onto a step that returns early orders nothing.
460
+ */
461
+ async function confirmVersionIsLive(probe, versionTag, identity, options, dependencies, cwd) {
462
+ const wait = dependencies.wait ?? {};
463
+ const live = await awaitLiveVersion(probe, versionTag, wait);
464
+ if (!live.ok)
465
+ throw new Error(`${identity.workerName}: ${live.error.message}`);
466
+ if (options.versionEndpoint) {
467
+ const answered = await awaitVersionEndpoint(options.versionEndpoint, versionTag, wait);
468
+ if (!answered.ok)
469
+ throw new Error(`${identity.workerName}: ${answered.error.message}`);
488
470
  }
489
- return Object.values(value).some((entry) => isFullCurrentDeployment(entry, versionId));
471
+ const processEnv = dependencies.processEnv ?? process.env;
472
+ const directory = dependencies.liveVersionCacheDirectory ?? liveVersionCacheDirectory(cwd, processEnv);
473
+ await writeCachedLiveVersion(directory, { accountId: identity.accountId, workerName: identity.workerName, stage: identity.stage }, { versionTag, versionId: live.value.versionId, fetchedAt: Date.now() });
490
474
  }
491
- async function wranglerJson(runner, args, run) {
475
+ /** One wrangler invocation whose stdout must be JSON; shared by the deploy and the read-only query. */
476
+ export async function wranglerJson(runner, args, run) {
492
477
  const result = await wrangler(runner, args, run);
493
478
  try {
494
479
  return JSON.parse(result.stdout);
@@ -0,0 +1,44 @@
1
+ import { type ProcessRunner } from './deploy-stage.js';
2
+ import { type DeploymentStage } from './stage.js';
3
+ export interface DeployedVersionOptions {
4
+ /** `staging`, `production`, or `prN`. */
5
+ stage: string;
6
+ /** The build-generated flat wrangler.json the deploy would use, when the project deploys one. */
7
+ config?: string;
8
+ /** Ask Cloudflare even when a fresh cache entry exists. */
9
+ refresh?: boolean;
10
+ }
11
+ export interface DeployedVersionDependencies {
12
+ runner?: ProcessRunner;
13
+ processEnv?: NodeJS.ProcessEnv;
14
+ cacheDirectory?: string;
15
+ ttlMs?: number;
16
+ now?: () => number;
17
+ }
18
+ export interface DeployedVersionReport {
19
+ workerName: string;
20
+ stage: DeploymentStage;
21
+ versionTag: string | null;
22
+ versionId: string;
23
+ source: 'cloudflare' | 'cache';
24
+ }
25
+ /**
26
+ * The worker `stage` deploys to, from committed configuration alone.
27
+ *
28
+ * A pull-request stage's worker name is derived from the staging name the same way the deploy
29
+ * derives it, but WITHOUT the account listing the deploy needs for its KV/D1/R2 isolation: a
30
+ * question about what is live must not be able to create anything.
31
+ */
32
+ export declare function stageWorkerName(cwd: string, stage: DeploymentStage, configPath?: string): Promise<string>;
33
+ /**
34
+ * Answers with the tag serving 100% of this worker's traffic, or refuses.
35
+ *
36
+ * Refusing is the whole contract. A missing credential, an API error, or a traffic split has no
37
+ * honest answer, and returning a placeholder such as `unknown` would be worse than the error: a
38
+ * later run comparing against that placeholder would read it as a match and conclude the desired
39
+ * version is already live. For the same reason nothing here is ever written to the cache except a
40
+ * real observation, and the answer never contains a timestamp or anything else that varies per
41
+ * invocation.
42
+ */
43
+ export declare function deployedVersion(cwd: string, options: DeployedVersionOptions, dependencies?: DeployedVersionDependencies): Promise<DeployedVersionReport>;
44
+ //# sourceMappingURL=deployed-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployed-version.d.ts","sourceRoot":"","sources":["../../src/wrangler/deployed-version.ts"],"names":[],"mappings":"AAUA,OAAO,EAAoB,KAAK,aAAa,EAAwC,MAAM,mBAAmB,CAAC;AAU/G,OAAO,EACL,KAAK,eAAe,EAMrB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,sBAAsB;IACrC,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAe/G;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,sBAAsB,EAC/B,YAAY,GAAE,2BAAgC,GAC7C,OAAO,CAAC,qBAAqB,CAAC,CAqChC"}
@@ -0,0 +1,87 @@
1
+ // `smoo wrangler deployed-version --stage <stage>` — what is serving this project's worker right now.
2
+ //
3
+ // Read-only by construction: it resolves the worker name from the committed config and asks
4
+ // Cloudflare. It never provisions, never derives a pull-request stage's resources, and never
5
+ // writes anything except its own cache entry.
6
+ import { existsSync } from 'node:fs';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { parseJsonFileText } from '../lib/json.js';
10
+ import { BunProcessRunner, wranglerJson } from './deploy-stage.js';
11
+ import { parseFlatWranglerConfig, planFlatStageResources } from './flat-config.js';
12
+ import { LIVE_VERSION_CACHE_TTL_MS, liveVersionCacheDirectory, readCachedLiveVersion, readLiveVersion, writeCachedLiveVersion, } from './live-version.js';
13
+ import { isPullRequestStage, parseDeploymentStage, planConfiguredStageResources, stageResourceName, stagingWorkerBaseName, } from './stage.js';
14
+ /**
15
+ * The worker `stage` deploys to, from committed configuration alone.
16
+ *
17
+ * A pull-request stage's worker name is derived from the staging name the same way the deploy
18
+ * derives it, but WITHOUT the account listing the deploy needs for its KV/D1/R2 isolation: a
19
+ * question about what is live must not be able to create anything.
20
+ */
21
+ export async function stageWorkerName(cwd, stage, configPath) {
22
+ if (configPath) {
23
+ const flat = parseJsonFileText(configPath, await readFile(configPath, 'utf8'), parseFlatWranglerConfig);
24
+ return isPullRequestStage(stage)
25
+ ? stageResourceName(stagingWorkerBaseName(flat.name), stage)
26
+ : planFlatStageResources(flat, stage).workerName;
27
+ }
28
+ const tomlPath = join(cwd, 'wrangler.toml');
29
+ if (!existsSync(tomlPath)) {
30
+ throw new Error(`${tomlPath} does not exist; pass --config for a build-generated flat configuration.`);
31
+ }
32
+ const toml = await readFile(tomlPath, 'utf8');
33
+ return isPullRequestStage(stage)
34
+ ? stageResourceName(stagingWorkerBaseName(planConfiguredStageResources(toml, 'staging').workerName), stage)
35
+ : planConfiguredStageResources(toml, stage).workerName;
36
+ }
37
+ /**
38
+ * Answers with the tag serving 100% of this worker's traffic, or refuses.
39
+ *
40
+ * Refusing is the whole contract. A missing credential, an API error, or a traffic split has no
41
+ * honest answer, and returning a placeholder such as `unknown` would be worse than the error: a
42
+ * later run comparing against that placeholder would read it as a match and conclude the desired
43
+ * version is already live. For the same reason nothing here is ever written to the cache except a
44
+ * real observation, and the answer never contains a timestamp or anything else that varies per
45
+ * invocation.
46
+ */
47
+ export async function deployedVersion(cwd, options, dependencies = {}) {
48
+ const stage = parseDeploymentStage(options.stage);
49
+ const processEnv = dependencies.processEnv ?? process.env;
50
+ const accountId = processEnv.CLOUDFLARE_ACCOUNT_ID;
51
+ if (!accountId)
52
+ throw new Error('CLOUDFLARE_ACCOUNT_ID is required.');
53
+ if (!processEnv.CLOUDFLARE_API_TOKEN)
54
+ throw new Error('CLOUDFLARE_API_TOKEN is required.');
55
+ const workerName = await stageWorkerName(cwd, stage, options.config);
56
+ const cacheDirectory = dependencies.cacheDirectory ?? liveVersionCacheDirectory(cwd, processEnv);
57
+ const key = { accountId, workerName, stage };
58
+ const ttlMs = dependencies.ttlMs ?? LIVE_VERSION_CACHE_TTL_MS;
59
+ const now = dependencies.now ?? Date.now;
60
+ if (options.refresh !== true) {
61
+ const cached = await readCachedLiveVersion(cacheDirectory, key, ttlMs, now);
62
+ if (cached) {
63
+ return { workerName, stage, versionTag: cached.versionTag, versionId: cached.versionId, source: 'cache' };
64
+ }
65
+ }
66
+ const runner = dependencies.runner ?? new BunProcessRunner();
67
+ const run = { cwd, unsetEnv: ['CLOUDFLARE_ENV'] };
68
+ const probe = {
69
+ deployments: () => wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run),
70
+ versions: () => wranglerJson(runner, ['versions', 'list', '--name', workerName, '--json'], run),
71
+ };
72
+ const live = await readLiveVersion(probe);
73
+ if (!live.ok)
74
+ throw new Error(`${workerName} (${stage}): ${live.error.message}`);
75
+ await writeCachedLiveVersion(cacheDirectory, key, {
76
+ versionTag: live.value.tag,
77
+ versionId: live.value.versionId,
78
+ fetchedAt: now(),
79
+ });
80
+ return {
81
+ workerName,
82
+ stage,
83
+ versionTag: live.value.tag,
84
+ versionId: live.value.versionId,
85
+ source: 'cloudflare',
86
+ };
87
+ }
@@ -0,0 +1,146 @@
1
+ /** Same shape as the release tooling's Result: a known operational failure is a value, not a throw. */
2
+ export type Result<T, E> = {
3
+ ok: true;
4
+ value: T;
5
+ } | {
6
+ ok: false;
7
+ error: E;
8
+ };
9
+ /** The version Cloudflare is serving to 100% of traffic for one worker. */
10
+ export interface LiveVersion {
11
+ versionId: string;
12
+ /** `wrangler versions deploy --version-tag` matches on this; a version deployed outside Nx has none. */
13
+ tag: string | null;
14
+ }
15
+ export type LiveVersionFailure = {
16
+ kind: 'never-deployed';
17
+ message: string;
18
+ } | {
19
+ kind: 'split-traffic';
20
+ message: string;
21
+ };
22
+ /** Reads the two `wrangler` JSON payloads the live version is derived from. */
23
+ export interface LiveVersionProbe {
24
+ /** `wrangler deployments status --name <worker> --json` */
25
+ deployments(): Promise<unknown>;
26
+ /** `wrangler versions list --name <worker> --json` */
27
+ versions(): Promise<unknown>;
28
+ }
29
+ /**
30
+ * Every `{ id -> tag }` pair the payload carries, in document order.
31
+ *
32
+ * Wrangler nests versions differently per subcommand and per version of itself, spells the tag
33
+ * three ways (`annotations['workers/tag']`, a bare `tag`, `metadata.tag`) and the id two (`id`,
34
+ * `version_id`). One walk collects the relation once; both directions of the lookup are then a Map
35
+ * read rather than a second recursive scan that could disagree with the first.
36
+ */
37
+ export declare function collectVersionTags(value: unknown): Map<string, string>;
38
+ /** The version id whose tag is `tag`, or null. */
39
+ export declare function findVersionIdByTag(value: unknown, tag: string): string | null;
40
+ /** One version's share of the current deployment's traffic. */
41
+ export interface DeployedVersionShare {
42
+ versionId: string | null;
43
+ percentage: number;
44
+ }
45
+ /**
46
+ * The current deployment's traffic split, or null when the payload names no deployment at all.
47
+ *
48
+ * "Nothing is deployed" and "two versions share the traffic" are different facts with different
49
+ * remedies, and a caller that collapses them cannot tell an operator which one it hit — so the
50
+ * split is returned whole and both questions are answered from it.
51
+ */
52
+ export declare function currentDeploymentVersions(value: unknown): DeployedVersionShare[] | null;
53
+ /**
54
+ * The one version serving 100% of traffic, or null when there is none or the deployment is split.
55
+ *
56
+ * A split is not "close enough": with two versions live, "is our version live" has no single
57
+ * answer, and answering it optimistically is how a deploy reports success while part of the
58
+ * traffic still reaches the old code.
59
+ */
60
+ export declare function currentDeploymentVersionId(value: unknown): string | null;
61
+ /** The version serving all traffic for this worker, with the tag it was uploaded under. */
62
+ export declare function readLiveVersion(probe: LiveVersionProbe): Promise<Result<LiveVersion, LiveVersionFailure>>;
63
+ /**
64
+ * How long a deploy waits for the version it just activated to become the one being served.
65
+ *
66
+ * The bound is the point: an unbounded loop turns a stuck propagation into a hung CI job nobody
67
+ * reads, and a fixed sleep turns it into a green job that lied. Both are worse than a red job
68
+ * naming what it expected and what it saw.
69
+ */
70
+ export declare const LIVE_VERSION_WAIT_BUDGET_MS = 120000;
71
+ /** Cloudflare's control plane converges in seconds; polling faster than this only burns API quota. */
72
+ export declare const LIVE_VERSION_POLL_INTERVAL_MS = 2000;
73
+ export interface LiveVersionWaitOptions {
74
+ budgetMs?: number;
75
+ intervalMs?: number;
76
+ now?: () => number;
77
+ sleep?: (ms: number) => Promise<void>;
78
+ }
79
+ export interface LiveVersionNotObserved {
80
+ kind: 'not-observed';
81
+ message: string;
82
+ expected: string;
83
+ observed: string;
84
+ }
85
+ /**
86
+ * Polls until the worker's live version carries `expectedTag`, or the budget runs out.
87
+ *
88
+ * The tag is the match, not the version id: the tag IS the desired-state identity (`nx-<task
89
+ * hash>`), it is what both `wrangler deploy --tag` and `wrangler versions deploy --version-tag`
90
+ * were told to make live, and it is the one name the caller knows before the upload has produced
91
+ * a version id.
92
+ *
93
+ * Returns the failure rather than throwing it: a deploy that cannot be observed is an operational
94
+ * outcome the caller has to report, not a broken invariant.
95
+ */
96
+ export declare function awaitLiveVersion(probe: LiveVersionProbe, expectedTag: string, options?: LiveVersionWaitOptions): Promise<Result<LiveVersion, LiveVersionNotObserved>>;
97
+ /**
98
+ * Exactly the call the wait makes. Demanding all of `typeof fetch` would force every caller — the
99
+ * tests included — to also supply `preconnect`, which nothing here uses.
100
+ */
101
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
102
+ export interface VersionEndpointWaitOptions extends LiveVersionWaitOptions {
103
+ fetch?: FetchLike;
104
+ }
105
+ /**
106
+ * Polls a worker-served endpoint until it answers with `expectedTag`.
107
+ *
108
+ * The control plane agreeing is necessary and not sufficient — that gap is exactly what this
109
+ * change's predecessor measured — so a project that can prove the edge serves the new code says so
110
+ * with an endpoint echoing its own version tag (Workers read it from the version-metadata
111
+ * binding). The contract is deliberately one shape: the trimmed response body IS the tag.
112
+ */
113
+ export declare function awaitVersionEndpoint(url: string, expectedTag: string, options?: VersionEndpointWaitOptions): Promise<Result<void, LiveVersionNotObserved>>;
114
+ /**
115
+ * How long `smoo wrangler deployed-version` trusts its own last answer.
116
+ *
117
+ * This TTL is a convenience for operators and repeated local queries, NOT a correctness mechanism:
118
+ * nothing that decides whether to deploy may read it. The deploy's own liveness check always calls
119
+ * Cloudflare, because a cached "live already equals the desired tag" is precisely the belief a
120
+ * rollback falsifies, and acting on it would skip the deploy that repairs the rollback.
121
+ */
122
+ export declare const LIVE_VERSION_CACHE_TTL_MS = 45000;
123
+ export interface LiveVersionCacheKey {
124
+ accountId: string;
125
+ workerName: string;
126
+ stage: string;
127
+ }
128
+ export interface CachedLiveVersion {
129
+ versionTag: string | null;
130
+ versionId: string;
131
+ fetchedAt: number;
132
+ }
133
+ /**
134
+ * One file per key, so two deploys running in parallel cannot clobber each other's answer; a
135
+ * shared map would need a lock to say the same thing.
136
+ */
137
+ export declare function liveVersionCachePath(cacheDirectory: string, key: LiveVersionCacheKey): string;
138
+ export declare function readCachedLiveVersion(cacheDirectory: string, key: LiveVersionCacheKey, ttlMs: number, now?: () => number): Promise<CachedLiveVersion | null>;
139
+ export declare function writeCachedLiveVersion(cacheDirectory: string, key: LiveVersionCacheKey, entry: CachedLiveVersion): Promise<void>;
140
+ /**
141
+ * Where the cache lives. Nx's workspace data directory already exists, is already gitignored, and
142
+ * is already wiped when the workspace is reset — three properties a hand-rolled directory would
143
+ * each have to re-earn, and one ($HOME) that would leak one checkout's answers into another's.
144
+ */
145
+ export declare function liveVersionCacheDirectory(workspaceRoot: string, environment: NodeJS.ProcessEnv): string;
146
+ //# sourceMappingURL=live-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-version.d.ts","sourceRoot":"","sources":["../../src/wrangler/live-version.ts"],"names":[],"mappings":"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"}