@smoothbricks/cli 0.11.16 → 0.11.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +47 -0
- package/dist/github-ci/index.d.ts +1 -1
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +7 -10
- package/dist/lib/secret-names.d.ts +35 -0
- package/dist/lib/secret-names.d.ts.map +1 -0
- package/dist/lib/secret-names.js +61 -0
- package/dist/monorepo/ci-workflow.d.ts +56 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +214 -8
- package/dist/monorepo/managed-files.d.ts +23 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +39 -1
- package/dist/monorepo/publish-workflow.d.ts +10 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +58 -20
- package/dist/release/github-release.d.ts +10 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +4 -5
- package/dist/secrets/commands.d.ts +84 -0
- package/dist/secrets/commands.d.ts.map +1 -0
- package/dist/secrets/commands.js +375 -0
- package/dist/secrets/index.d.ts +103 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +255 -0
- package/dist/secrets/repository.d.ts +60 -0
- package/dist/secrets/repository.d.ts.map +1 -0
- package/dist/secrets/repository.js +145 -0
- package/dist/wrangler/cloudflare.d.ts +7 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -1
- package/dist/wrangler/cloudflare.js +10 -0
- package/dist/wrangler/deploy-stage.d.ts +13 -2
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +89 -78
- package/dist/wrangler/deployed-version.d.ts +44 -0
- package/dist/wrangler/deployed-version.d.ts.map +1 -0
- package/dist/wrangler/deployed-version.js +87 -0
- package/dist/wrangler/live-version.d.ts +146 -0
- package/dist/wrangler/live-version.d.ts.map +1 -0
- package/dist/wrangler/live-version.js +326 -0
- package/dist/wrangler/stage-secrets.d.ts +57 -0
- package/dist/wrangler/stage-secrets.d.ts.map +1 -0
- package/dist/wrangler/stage-secrets.js +178 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
- package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
- package/package.json +2 -2
- package/src/cli.ts +69 -1
- package/src/github-ci/index.test.ts +92 -8
- package/src/github-ci/index.ts +7 -10
- package/src/lib/secret-names.ts +70 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +188 -2
- package/src/monorepo/__tests__/publish-workflow.test.ts +33 -15
- package/src/monorepo/ci-workflow.ts +294 -8
- package/src/monorepo/managed-files.test.ts +28 -1
- package/src/monorepo/managed-files.ts +56 -2
- package/src/monorepo/package-policy.test.ts +1 -1
- package/src/monorepo/publish-workflow.ts +65 -19
- package/src/monorepo/secret-references.test.ts +1 -1
- package/src/release/__tests__/github-release.test.ts +8 -4
- package/src/release/__tests__/private-npm-status.test.ts +2 -2
- package/src/release/github-release.ts +13 -5
- package/src/release/index.ts +4 -4
- package/src/secrets/commands.test.ts +28 -0
- package/src/secrets/commands.ts +412 -0
- package/src/secrets/index.test.ts +209 -0
- package/src/secrets/index.ts +287 -0
- package/src/secrets/repository.test.ts +98 -0
- package/src/secrets/repository.ts +164 -0
- package/src/wrangler/cloudflare.ts +18 -0
- package/src/wrangler/deploy-stage.test.ts +448 -23
- package/src/wrangler/deploy-stage.ts +142 -83
- package/src/wrangler/deployed-version.test.ts +150 -0
- package/src/wrangler/deployed-version.ts +130 -0
- package/src/wrangler/live-version.ts +363 -0
- package/src/wrangler/stage-secrets.ts +146 -0
|
@@ -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
|
-
|
|
64
|
-
export declare function
|
|
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":"
|
|
1
|
+
{"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import typia from 'typia';
|
|
6
4
|
import { parseJsonFileText } from '../lib/json.js';
|
|
7
5
|
import { mergeEnv, printCommandOutput } from '../lib/run.js';
|
|
8
6
|
import { CloudflareRestClient } from './cloudflare.js';
|
|
9
7
|
import { parseFlatWranglerConfig, planFlatStageResources } from './flat-config.js';
|
|
10
|
-
import {
|
|
8
|
+
import { awaitLiveVersion, awaitVersionEndpoint, currentDeploymentVersionId, findVersionIdByTag, liveVersionCacheDirectory, writeCachedLiveVersion, } from './live-version.js';
|
|
11
9
|
import { derivePullRequestStageConfig, derivePullRequestWranglerConfig, hasExactStageSegment, isPullRequestStage, parseDeploymentStage, planConfiguredStageResources, planPullRequestBindings, planPullRequestResources, pullRequestStage, } from './stage.js';
|
|
10
|
+
import { planStageSecrets, readDeclaredSecretNames, readSecretStageMap, stageSecretRefusal, } from './stage-secrets.js';
|
|
12
11
|
/** The child environment `options` describe; every runner, test doubles included, spawns with exactly this. */
|
|
13
12
|
export function childEnvironment(options) {
|
|
14
13
|
return mergeEnv(options.env, options.unsetEnv);
|
|
@@ -30,15 +29,6 @@ export class BunProcessRunner {
|
|
|
30
29
|
return { exitCode, stdout, stderr };
|
|
31
30
|
}
|
|
32
31
|
}
|
|
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
32
|
export async function deployStage(cwd, options, dependencies = {}) {
|
|
43
33
|
const stage = parseDeploymentStage(options.stage);
|
|
44
34
|
const processEnv = dependencies.processEnv ?? process.env;
|
|
@@ -49,21 +39,26 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
49
39
|
const cloudflare = dependencies.cloudflare ??
|
|
50
40
|
new CloudflareRestClient(accountId, requiredEnvironmentValue(apiToken, 'CLOUDFLARE_API_TOKEN'));
|
|
51
41
|
const runner = dependencies.runner ?? new BunProcessRunner();
|
|
52
|
-
const
|
|
42
|
+
const secretPlan = planStageSecrets(readDeclaredSecretNames(cwd), stage, readSecretStageMap(cwd));
|
|
43
|
+
// Only the stage's own secrets, and only the ones a value exists for. A secret scoped to other
|
|
44
|
+
// stages is dropped here even when this shell exports it: that is the whole permission rule.
|
|
53
45
|
const secretValues = {};
|
|
54
|
-
for (const name of
|
|
46
|
+
for (const name of secretPlan.required) {
|
|
55
47
|
const value = processEnv[name];
|
|
56
48
|
if (value)
|
|
57
49
|
secretValues[name] = value;
|
|
58
50
|
}
|
|
59
|
-
const
|
|
51
|
+
const gate = stageSecretGate(secretPlan, new Set(Object.keys(secretValues)), cloudflare);
|
|
60
52
|
let temporaryConfigPath;
|
|
61
53
|
let temporarySecretsPath;
|
|
62
54
|
try {
|
|
63
55
|
const prepared = options.config
|
|
64
|
-
? await prepareFlatConfig(options.config, stage, accountId,
|
|
65
|
-
: await prepareTomlConfig(cwd, stage, accountId,
|
|
56
|
+
? await prepareFlatConfig(options.config, stage, accountId, gate, cloudflare)
|
|
57
|
+
: await prepareTomlConfig(cwd, stage, accountId, gate, cloudflare);
|
|
66
58
|
temporaryConfigPath = prepared.temporaryConfigPath;
|
|
59
|
+
// The pull-request path already ran this before provisioning; the gate answers once per Worker.
|
|
60
|
+
// Everything below here writes to Cloudflare.
|
|
61
|
+
await gate(prepared.plan.workerName);
|
|
67
62
|
const workerExists = await reconcileStageResources(prepared.plan, cloudflare);
|
|
68
63
|
const versionTag = nxTaskVersionTag(processEnv);
|
|
69
64
|
const envArgs = prepared.envFlag ? ['--env', prepared.envFlag] : [];
|
|
@@ -71,17 +66,31 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
71
66
|
// renames the worker `<name>-<CLOUDFLARE_ENV>`. The build that produced the flat config is the
|
|
72
67
|
// caller that sets the variable, so the deploy would silently succeed under a name neither
|
|
73
68
|
// `reconcileStageResources` nor `versions list` looks at.
|
|
74
|
-
|
|
69
|
+
//
|
|
70
|
+
// A secret this stage is scoped out of goes with it. Leaving it out of the secrets payload is
|
|
71
|
+
// what stops it being installed; withholding it from the child as well means the deploy cannot
|
|
72
|
+
// read it at all, so "this stage never sees that capability" holds at the process boundary and
|
|
73
|
+
// not merely in the file we happen to write.
|
|
74
|
+
const unsetEnv = [...(prepared.envFlag ? [] : ['CLOUDFLARE_ENV']), ...secretPlan.withheld];
|
|
75
|
+
const run = unsetEnv.length > 0 ? { cwd, unsetEnv } : { cwd };
|
|
75
76
|
const workerName = prepared.plan.workerName;
|
|
77
|
+
const probe = {
|
|
78
|
+
deployments: () => wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run),
|
|
79
|
+
versions: () => wranglerJson(runner, ['versions', 'list', '--name', workerName, '--json'], run),
|
|
80
|
+
};
|
|
76
81
|
// The tagged-version lookup runs before the migrations: a cache hit means this exact build is
|
|
77
82
|
// already live, so its migrations ran with it and re-applying them would touch the remote
|
|
78
83
|
// database for nothing. The activation and upload paths below still migrate first.
|
|
84
|
+
//
|
|
85
|
+
// This reads Cloudflare every time and never the local cache. A cache hit here would mean
|
|
86
|
+
// "we believe this hash is live" — exactly the belief a rollback falsifies — and believing it
|
|
87
|
+
// would skip the deploy that repairs the rollback. Reading live state is what makes a
|
|
88
|
+
// redundant deploy a proven no-op instead of an assumed one, which is in turn what makes a
|
|
89
|
+
// cross-project `dependsOn: ["<other>:deploy"]` edge cheap enough to be the ordering mechanism.
|
|
79
90
|
let taggedVersionId = null;
|
|
80
91
|
if (versionTag && workerExists) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
taggedVersionId = findVersionIdByTag(versions, versionTag);
|
|
84
|
-
if (taggedVersionId && isFullCurrentDeployment(deployments, taggedVersionId)) {
|
|
92
|
+
taggedVersionId = findVersionIdByTag(await probe.versions(), versionTag);
|
|
93
|
+
if (taggedVersionId && currentDeploymentVersionId(await probe.deployments()) === taggedVersionId) {
|
|
85
94
|
return { stage, workerName, action: 'remote-cache-hit', versionTag };
|
|
86
95
|
}
|
|
87
96
|
}
|
|
@@ -101,6 +110,7 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
101
110
|
...envArgs,
|
|
102
111
|
'--yes',
|
|
103
112
|
], run);
|
|
113
|
+
await confirmVersionIsLive(probe, versionTag, { stage, workerName, accountId }, options, dependencies, cwd);
|
|
104
114
|
return { stage, workerName, action: 'activated', versionTag };
|
|
105
115
|
}
|
|
106
116
|
const deployArgs = ['deploy', '--config', prepared.deployConfigPath, ...envArgs];
|
|
@@ -112,6 +122,9 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
112
122
|
deployArgs.push('--secrets-file', temporarySecretsPath);
|
|
113
123
|
}
|
|
114
124
|
await wrangler(runner, deployArgs, run);
|
|
125
|
+
if (versionTag) {
|
|
126
|
+
await confirmVersionIsLive(probe, versionTag, { stage, workerName, accountId }, options, dependencies, cwd);
|
|
127
|
+
}
|
|
115
128
|
return { stage, workerName, action: 'deployed', ...(versionTag ? { versionTag } : {}) };
|
|
116
129
|
}
|
|
117
130
|
finally {
|
|
@@ -123,7 +136,7 @@ export async function deployStage(cwd, options, dependencies = {}) {
|
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
|
-
async function prepareTomlConfig(cwd, stage, accountId,
|
|
139
|
+
async function prepareTomlConfig(cwd, stage, accountId, gate, cloudflare) {
|
|
127
140
|
const committedConfigPath = join(cwd, 'wrangler.toml');
|
|
128
141
|
const committedToml = await readFile(committedConfigPath, 'utf8');
|
|
129
142
|
if (!isPullRequestStage(stage)) {
|
|
@@ -136,7 +149,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
|
|
|
136
149
|
}
|
|
137
150
|
const liveNamespaces = await cloudflare.listKvNamespaces();
|
|
138
151
|
const plan = planPullRequestResources(committedToml, stage, liveNamespaces);
|
|
139
|
-
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan,
|
|
152
|
+
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
|
|
140
153
|
const derivedToml = derivePullRequestWranglerConfig(committedToml, {
|
|
141
154
|
stage,
|
|
142
155
|
accountId,
|
|
@@ -153,7 +166,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
|
|
|
153
166
|
envFlag: stage,
|
|
154
167
|
};
|
|
155
168
|
}
|
|
156
|
-
async function prepareFlatConfig(configPath, stage, accountId,
|
|
169
|
+
async function prepareFlatConfig(configPath, stage, accountId, gate, cloudflare) {
|
|
157
170
|
const flat = parseJsonFileText(configPath, await readFile(configPath, 'utf8'), parseFlatWranglerConfig);
|
|
158
171
|
if (!isPullRequestStage(stage)) {
|
|
159
172
|
return {
|
|
@@ -164,7 +177,7 @@ async function prepareFlatConfig(configPath, stage, accountId, missingSecrets, c
|
|
|
164
177
|
}
|
|
165
178
|
const liveNamespaces = await cloudflare.listKvNamespaces();
|
|
166
179
|
const plan = planPullRequestBindings(flat, stage, liveNamespaces);
|
|
167
|
-
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan,
|
|
180
|
+
const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
|
|
168
181
|
const derived = derivePullRequestStageConfig(flat, { stage, accountId, kvNamespaceIds, d1DatabaseIds });
|
|
169
182
|
// Beside the original: its main/assets/migrations paths are relative to the file.
|
|
170
183
|
const temporaryConfigPath = join(dirname(configPath), `.wrangler.smoo-${process.pid}-${randomUUID()}.json`);
|
|
@@ -196,15 +209,35 @@ function migrationBindings(config) {
|
|
|
196
209
|
.filter((database) => typeof database.migrations_dir === 'string')
|
|
197
210
|
.map((database) => database.binding);
|
|
198
211
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
/**
|
|
213
|
+
* One answer per Worker, reused by both call sites so the read costs one round trip.
|
|
214
|
+
*
|
|
215
|
+
* What the Worker holds comes from the account's script listing first. A Worker absent from it
|
|
216
|
+
* holds nothing — exactly the first-deployment case — and Cloudflare answers 404 rather than an
|
|
217
|
+
* empty list when asked for a missing Worker's secrets. Reading that failure as "then nothing is
|
|
218
|
+
* needed" would disarm the check at the one moment it matters most, so it is never asked.
|
|
219
|
+
*/
|
|
220
|
+
function stageSecretGate(plan, exported, cloudflare) {
|
|
221
|
+
const answered = new Map();
|
|
222
|
+
return (workerName) => {
|
|
223
|
+
let pending = answered.get(workerName);
|
|
224
|
+
if (!pending) {
|
|
225
|
+
pending = assertStageSecrets(plan, exported, cloudflare, workerName);
|
|
226
|
+
answered.set(workerName, pending);
|
|
227
|
+
}
|
|
228
|
+
return pending;
|
|
229
|
+
};
|
|
204
230
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await
|
|
231
|
+
async function assertStageSecrets(plan, exported, cloudflare, workerName) {
|
|
232
|
+
const live = (await cloudflare.listWorkerScripts()).some((script) => script.id === workerName);
|
|
233
|
+
const held = new Set(live ? await cloudflare.listWorkerSecrets(workerName) : []);
|
|
234
|
+
const refusal = stageSecretRefusal(plan, exported, held, workerName);
|
|
235
|
+
if (refusal)
|
|
236
|
+
throw new Error(refusal);
|
|
237
|
+
}
|
|
238
|
+
/** Isolation refusals already ran in the plan; the gate below is the last one before the first write. */
|
|
239
|
+
async function provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces) {
|
|
240
|
+
await gate(plan.workerName);
|
|
208
241
|
const kvNamespaceIds = await ensureKvNamespaces(plan.kvNamespaces, liveNamespaces, cloudflare);
|
|
209
242
|
const d1DatabaseIds = plan.d1Databases.length === 0
|
|
210
243
|
? new Map()
|
|
@@ -447,48 +480,30 @@ export function nxTaskVersionTag(environment) {
|
|
|
447
480
|
}
|
|
448
481
|
throw new Error('NX_TASK_HASH must be canonical decimal digits or at least 32 hexadecimal characters.');
|
|
449
482
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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));
|
|
483
|
+
/**
|
|
484
|
+
* Turns "Cloudflare accepted the traffic shift" into "the new version answers".
|
|
485
|
+
*
|
|
486
|
+
* Without this the deploy step goes green while the edge still serves the previous version — the
|
|
487
|
+
* measured gap was about 20 s — so anything ordered after this deploy could call into code that
|
|
488
|
+
* has not shipped yet. The wait is what a `dependsOn: ["<other>:deploy"]` edge actually buys; an
|
|
489
|
+
* edge onto a step that returns early orders nothing.
|
|
490
|
+
*/
|
|
491
|
+
async function confirmVersionIsLive(probe, versionTag, identity, options, dependencies, cwd) {
|
|
492
|
+
const wait = dependencies.wait ?? {};
|
|
493
|
+
const live = await awaitLiveVersion(probe, versionTag, wait);
|
|
494
|
+
if (!live.ok)
|
|
495
|
+
throw new Error(`${identity.workerName}: ${live.error.message}`);
|
|
496
|
+
if (options.versionEndpoint) {
|
|
497
|
+
const answered = await awaitVersionEndpoint(options.versionEndpoint, versionTag, wait);
|
|
498
|
+
if (!answered.ok)
|
|
499
|
+
throw new Error(`${identity.workerName}: ${answered.error.message}`);
|
|
488
500
|
}
|
|
489
|
-
|
|
501
|
+
const processEnv = dependencies.processEnv ?? process.env;
|
|
502
|
+
const directory = dependencies.liveVersionCacheDirectory ?? liveVersionCacheDirectory(cwd, processEnv);
|
|
503
|
+
await writeCachedLiveVersion(directory, { accountId: identity.accountId, workerName: identity.workerName, stage: identity.stage }, { versionTag, versionId: live.value.versionId, fetchedAt: Date.now() });
|
|
490
504
|
}
|
|
491
|
-
|
|
505
|
+
/** One wrangler invocation whose stdout must be JSON; shared by the deploy and the read-only query. */
|
|
506
|
+
export async function wranglerJson(runner, args, run) {
|
|
492
507
|
const result = await wrangler(runner, args, run);
|
|
493
508
|
try {
|
|
494
509
|
return JSON.parse(result.stdout);
|
|
@@ -510,7 +525,3 @@ function requiredEnvironmentValue(value, name) {
|
|
|
510
525
|
throw new Error(`${name} is required.`);
|
|
511
526
|
return value;
|
|
512
527
|
}
|
|
513
|
-
function readSecretNames(cwd) {
|
|
514
|
-
const path = join(cwd, '.dev.vars.example');
|
|
515
|
-
return existsSync(path) ? parseDevVarsExample(readFileSync(path, 'utf8')) : [];
|
|
516
|
-
}
|
|
@@ -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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"}
|