run402 4.105.5 → 4.106.0
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/lib/command-manifest.mjs +2 -0
- package/lib/repos.mjs +33 -0
- package/lib/transfer.mjs +78 -5
- package/lib/up.mjs +2 -1
- package/package.json +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +76 -13
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +10 -7
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/repos.d.ts +50 -0
- package/sdk/dist/namespaces/repos.d.ts.map +1 -1
- package/sdk/dist/namespaces/repos.js +55 -0
- package/sdk/dist/namespaces/repos.js.map +1 -1
- package/sdk/dist/namespaces/transfers.d.ts +91 -0
- package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
- package/sdk/dist/namespaces/transfers.js +12 -0
- package/sdk/dist/namespaces/transfers.js.map +1 -1
- package/sdk/dist/node/vault-publication.d.ts +24 -0
- package/sdk/dist/node/vault-publication.d.ts.map +1 -1
- package/sdk/dist/node/vault-publication.js +127 -81
- package/sdk/dist/node/vault-publication.js.map +1 -1
- package/vault-surface.json +2 -1
package/lib/command-manifest.mjs
CHANGED
|
@@ -206,6 +206,7 @@ export const COMMAND_MANIFEST = [
|
|
|
206
206
|
{ path: ["transfer", "preview"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
|
|
207
207
|
{ path: ["transfer", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
208
208
|
{ path: ["transfer", "accept"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
|
|
209
|
+
{ path: ["transfer", "handover"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
|
|
209
210
|
{ path: ["transfer", "cancel"], positionals: [p("transfer_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["tr_gate1"] },
|
|
210
211
|
|
|
211
212
|
// ── orgs ──────────────────────────────────────────────────────────────────
|
|
@@ -310,6 +311,7 @@ export const COMMAND_MANIFEST = [
|
|
|
310
311
|
{ path: ["repos", "access"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live org encryption-key directory + vault envelope recipients" },
|
|
311
312
|
{ path: ["repos", "access", "repair"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--recipient-state-version", "0", "--recipient-revocation-version", "0"], runStyle: "sub", skipBehavioral: "owner+step-up-gated epoch rotation (D193-D203, rev 42) — samples a fresh epoch key and re-seals a live vault's recipients" },
|
|
312
313
|
{ path: ["repos", "access", "revoke-key"], positionals: [p("principal_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["prin_00000000000000000000000000000000"], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares a recipient's key revoked (org-scoped watermark) and drives a real epoch rotation off it" },
|
|
314
|
+
{ path: ["repos", "access", "retire-previous-owner"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "owner+step-up-gated: blocks the previous owner's writer keys after a project transfer and drives a real epoch rotation" },
|
|
313
315
|
{ path: ["repos", "access", "declare-exposure"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares this vault's epoch secret exposed, forcing every subsequent ordinary push to refuse until a rotation lands" },
|
|
314
316
|
{ path: ["repos", "access", "sync"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "vault-multi-writer (rev 47): on-demand writer-admission reconcile — resolves pending candidates' signing keys via the live org encryption-key directory and publishes a real add_writer_key head per candidate" },
|
|
315
317
|
{ path: ["repos", "recover"], positionals: [p("source")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["s3://example-mirror-bucket", "--out", "__SCRATCH_DIR__/recover-out"], runStyle: "sub", skipBehavioral: "materializes a git repository from a mirror source, offline, with no server call" },
|
package/lib/repos.mjs
CHANGED
|
@@ -107,6 +107,7 @@ Maintenance:
|
|
|
107
107
|
run402 repos access [--project <project_id>] [--repo <repo_id>] [--human]
|
|
108
108
|
run402 repos access repair [--project <project_id>] [--repo <repo_id>] --recipient-state-version <n> --recipient-revocation-version <n>
|
|
109
109
|
run402 repos access revoke-key <principal_id> [--project <project_id>] [--repo <repo_id>]
|
|
110
|
+
run402 repos access retire-previous-owner [--project <project_id>] [--repo <repo_id>]
|
|
110
111
|
run402 repos access declare-exposure [--project <project_id>] [--repo <repo_id>]
|
|
111
112
|
run402 repos access repin [--project <project_id>] [--repo <repo_id>] --principal <principal_id> --fingerprint <ek_fingerprint>
|
|
112
113
|
run402 repos policy <required|grandfathered> [--project <project_id>] [--repo <repo_id>] [--reason <why>]
|
|
@@ -358,6 +359,13 @@ Subcommands:
|
|
|
358
359
|
that declaration's OWN returned counters — no flags needed.
|
|
359
360
|
Owner + step-up. The rekey remedy for "this specific principal's
|
|
360
361
|
key should no longer be trusted."
|
|
362
|
+
access retire-previous-owner
|
|
363
|
+
After a project transfer: blocks every writer key whose principal
|
|
364
|
+
is not a developer+ member of the vault's organization (the
|
|
365
|
+
previous owner's writers) and commits the writer_key_revoked
|
|
366
|
+
rotation that removes them. A transfer never does this on its own.
|
|
367
|
+
Protects only what is written afterwards. Owner + step-up. Safe to
|
|
368
|
+
re-run; nothing to retire means no rotation.
|
|
361
369
|
access declare-exposure
|
|
362
370
|
Declares reason:"epoch_secret_exposed" for THIS vault
|
|
363
371
|
(vault-scoped, not org-wide) — the rekey remedy for a leaked
|
|
@@ -2941,6 +2949,30 @@ async function accessRevokeKey(args) {
|
|
|
2941
2949
|
* fingerprint (the out-of-band verification point) and moves the local pin.
|
|
2942
2950
|
* Adapter only: `sdk.repos.acceptRecipientKeyChange`.
|
|
2943
2951
|
*/
|
|
2952
|
+
async function accessRetirePreviousOwner(args) {
|
|
2953
|
+
const a = normalizeArgv(args);
|
|
2954
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--idempotency-key", "-v", "--verbose", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--idempotency-key"]);
|
|
2955
|
+
requirePositionalCount(a, [...COMMON_VALUE_FLAGS, "--idempotency-key"], { min: 0, max: 0, command: "run402 repos access retire-previous-owner", missing: "" });
|
|
2956
|
+
const sdk = getSdk();
|
|
2957
|
+
const target = await vaultTarget(a);
|
|
2958
|
+
try {
|
|
2959
|
+
const result = await sdk.repos.retirePreviousOwnerWriters({
|
|
2960
|
+
...target,
|
|
2961
|
+
...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
|
|
2962
|
+
});
|
|
2963
|
+
printJson(sdk, result);
|
|
2964
|
+
if (result.retired.length === 0) {
|
|
2965
|
+
console.error("no writer outside this vault's organization — nothing to retire.");
|
|
2966
|
+
} else {
|
|
2967
|
+
console.error(`retired ${result.retired.length} writer key(s) from outside the organization and rotated to epoch ${result.rotation?.new_epoch} at generation ${result.rotation?.generation}.`);
|
|
2968
|
+
}
|
|
2969
|
+
console.error(result.note);
|
|
2970
|
+
printVerboseStats(a, sdk);
|
|
2971
|
+
} catch (err) {
|
|
2972
|
+
reportSdkError(err);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2944
2976
|
async function accessRepin(args) {
|
|
2945
2977
|
const a = normalizeArgv(args);
|
|
2946
2978
|
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint"]);
|
|
@@ -3019,6 +3051,7 @@ async function access(args) {
|
|
|
3019
3051
|
const a = normalizeArgv(args);
|
|
3020
3052
|
if (a[0] === "repair") return accessRepair(a.slice(1));
|
|
3021
3053
|
if (a[0] === "revoke-key") return accessRevokeKey(a.slice(1));
|
|
3054
|
+
if (a[0] === "retire-previous-owner") return accessRetirePreviousOwner(a.slice(1));
|
|
3022
3055
|
if (a[0] === "declare-exposure") return accessDeclareExposure(a.slice(1));
|
|
3023
3056
|
if (a[0] === "repin") return accessRepin(a.slice(1));
|
|
3024
3057
|
if (a[0] === "sync") return accessSync(a.slice(1));
|
package/lib/transfer.mjs
CHANGED
|
@@ -18,6 +18,7 @@ Usage:
|
|
|
18
18
|
run402 transfer preview <transfer_id>
|
|
19
19
|
run402 transfer list [--incoming | --outgoing] [--limit N] [--after <cursor>]
|
|
20
20
|
run402 transfer accept <transfer_id> [--org <org_id>] [--accept-retained-member]
|
|
21
|
+
run402 transfer handover <transfer_id>
|
|
21
22
|
run402 transfer cancel <transfer_id> [--reason <text>]
|
|
22
23
|
|
|
23
24
|
Subcommands:
|
|
@@ -28,7 +29,10 @@ Subcommands:
|
|
|
28
29
|
list List pending transfers (incoming default, or --outgoing) — pending rows unioned
|
|
29
30
|
accept Accept an incoming transfer, whatever its address (wallet: your wallet must be
|
|
30
31
|
the to_wallet; email: your verified email must match; --org <org_id> picks the
|
|
31
|
-
receiving org, omit = new org)
|
|
32
|
+
receiving org, omit = new org). For a project with a vault, accept first
|
|
33
|
+
names you as the recipient of its source and waits for the sender's handover
|
|
34
|
+
handover Sender: hand the project's vault to the recipient they named (admit their key
|
|
35
|
+
as a writer and wrap the source key for them), so they can accept
|
|
32
36
|
cancel Cancel a pending transfer of any kind
|
|
33
37
|
|
|
34
38
|
Notes:
|
|
@@ -38,6 +42,10 @@ Notes:
|
|
|
38
42
|
- --to-org is same-actor only in the first gateway release: you must own both orgs.
|
|
39
43
|
- Secret VALUES are inherited by the recipient on completion; rotation is advised.
|
|
40
44
|
- GitHub repo ownership is NOT transferred — handle that out of band.
|
|
45
|
+
- A project's KyGit vault moves with it. The recipient cannot accept until the sender
|
|
46
|
+
runs 'transfer handover'; afterwards the new owner retires the previous owner's
|
|
47
|
+
writers with 'run402 repos access retire-previous-owner' when ready.
|
|
48
|
+
- A project whose vault stores its source in your own bucket (BYO) is not transferable.
|
|
41
49
|
`;
|
|
42
50
|
|
|
43
51
|
const SUB_HELP = {
|
|
@@ -101,10 +109,27 @@ owner's CI bindings on the project, enqueues notifications to both parties,
|
|
|
101
109
|
stamps a 'secrets_rotation_advised' advisory on the project, and returns the
|
|
102
110
|
project keys (saved to your keystore).
|
|
103
111
|
|
|
112
|
+
For a project with a KyGit vault, the source is handed over before accept: on
|
|
113
|
+
the first run accept publishes this machine's keystore identity, names you as the
|
|
114
|
+
principal that receives the source, and stops with TRANSFER_VAULT_HANDOVER_PENDING
|
|
115
|
+
until the sender runs 'run402 transfer handover <transfer_id>'. Run accept again
|
|
116
|
+
once 'transfer preview' shows vault_handover.state = complete.
|
|
117
|
+
|
|
104
118
|
Options:
|
|
105
119
|
--org <org_id> Email-addressed only: the org to receive the project (omit = new org).
|
|
106
120
|
--accept-retained-member Email-addressed only: accept the sender's retained-developer-membership
|
|
107
121
|
offer (see 'transfer preview' retain_member). Omit = full severance.
|
|
122
|
+
`,
|
|
123
|
+
handover: `run402 transfer handover — Hand a transferred project's vault to the recipient
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
run402 transfer handover <transfer_id>
|
|
127
|
+
|
|
128
|
+
The sender's step. Reads the principal the recipient named (they run 'transfer
|
|
129
|
+
accept' first), admits that principal's own published signing key as a writer of
|
|
130
|
+
the project's vault, and wraps the vault's current key for it. Your key must be a
|
|
131
|
+
writer of the vault. Safe to re-run: a step already done is reported as such.
|
|
132
|
+
The recipient can accept once vault_handover.state is complete.
|
|
108
133
|
`,
|
|
109
134
|
cancel: `run402 transfer cancel — Cancel a pending transfer
|
|
110
135
|
|
|
@@ -307,12 +332,57 @@ async function accept(args) {
|
|
|
307
332
|
const acceptRetain = parsedArgs.includes("--accept-retained-member");
|
|
308
333
|
walletAuthHeaders(`/agent/v1/transfers/${transferId}/accept`);
|
|
309
334
|
|
|
335
|
+
const sdk = getSdk();
|
|
336
|
+
const doAccept = () => sdk.admin.transfers.accept(transferId, {
|
|
337
|
+
orgId: orgId ?? undefined,
|
|
338
|
+
acceptRetainedMember: acceptRetain || undefined,
|
|
339
|
+
});
|
|
310
340
|
try {
|
|
311
|
-
const data = await
|
|
312
|
-
orgId: orgId ?? undefined,
|
|
313
|
-
acceptRetainedMember: acceptRetain || undefined,
|
|
314
|
-
});
|
|
341
|
+
const data = await doAccept();
|
|
315
342
|
console.log(JSON.stringify(data, null, 2));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// A project with a vault waits on its handover. When no recipient has
|
|
345
|
+
// been named yet, name this principal (publishing its keystore identity
|
|
346
|
+
// first) so the sender's 'transfer handover' has a key to hand over to.
|
|
347
|
+
const handoverState = err?.code === "TRANSFER_VAULT_HANDOVER_PENDING" ? err?.details?.vault_handover?.state : null;
|
|
348
|
+
if (handoverState !== "awaiting_recipient") {
|
|
349
|
+
reportSdkError(err);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
await sdk.repos.publishKeystoreIdentity();
|
|
354
|
+
const nominated = await sdk.admin.transfers.nominateVaultRecipient(transferId);
|
|
355
|
+
if (nominated.vault_handover?.state === "complete") {
|
|
356
|
+
console.log(JSON.stringify(await doAccept(), null, 2));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
fail({
|
|
360
|
+
code: "TRANSFER_VAULT_HANDOVER_PENDING",
|
|
361
|
+
message: "You are named as the recipient of this project's source. The sender must run 'run402 transfer handover " + transferId + "' (they have been notified); then run this accept again.",
|
|
362
|
+
details: { transfer_id: transferId, vault_handover: nominated.vault_handover },
|
|
363
|
+
next_actions: nominated.next_actions,
|
|
364
|
+
retryable: true,
|
|
365
|
+
});
|
|
366
|
+
} catch (inner) {
|
|
367
|
+
reportSdkError(inner);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function handover(args) {
|
|
373
|
+
const parsedArgs = normalizeArgv(args);
|
|
374
|
+
assertKnownFlags(parsedArgs, ["--help", "-h"], []);
|
|
375
|
+
const positionals = positionalArgs(parsedArgs, []);
|
|
376
|
+
if (positionals.length !== 1) {
|
|
377
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 transfer handover <transfer_id>" });
|
|
378
|
+
}
|
|
379
|
+
const transferId = positionals[0];
|
|
380
|
+
try {
|
|
381
|
+
const data = await getSdk().repos.completeTransferHandover(transferId);
|
|
382
|
+
console.log(JSON.stringify(data, null, 2));
|
|
383
|
+
if (data.vault_handover?.state === "complete") {
|
|
384
|
+
console.error(`handed over: the recipient can now run 'run402 transfer accept ${transferId}'.`);
|
|
385
|
+
}
|
|
316
386
|
} catch (err) {
|
|
317
387
|
reportSdkError(err);
|
|
318
388
|
}
|
|
@@ -361,6 +431,9 @@ export async function run(sub, args) {
|
|
|
361
431
|
case "accept":
|
|
362
432
|
await accept(args);
|
|
363
433
|
return;
|
|
434
|
+
case "handover":
|
|
435
|
+
await handover(args);
|
|
436
|
+
return;
|
|
364
437
|
case "cancel":
|
|
365
438
|
await cancel(args);
|
|
366
439
|
return;
|
package/lib/up.mjs
CHANGED
|
@@ -65,7 +65,8 @@ Options:
|
|
|
65
65
|
commits directly, and a plan whose migrations are all
|
|
66
66
|
already applied with an identical checksum is not
|
|
67
67
|
rehearsed either (result.deploy.rehearsal says which:
|
|
68
|
-
no_live_release / migrations_unchanged / no_migrations
|
|
68
|
+
no_live_release / migrations_unchanged / no_migrations /
|
|
69
|
+
ci_session_unsupported).
|
|
69
70
|
--repo-only Provision + scaffold the run402 remote + first push,
|
|
70
71
|
and stop there — no deploy. The vault-only track
|
|
71
72
|
(D8), composed through up instead of run402 repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.106.0",
|
|
4
4
|
"description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ import { isGrantKeyCredentials } from "../grant-key-credentials.js";
|
|
|
24
24
|
import { isKnownBinaryContent, isUtf8StringSource } from "../validation.js";
|
|
25
25
|
import { assertCiDeployableSpec } from "./ci.js";
|
|
26
26
|
import { ROUTE_PRICING_NETWORKS, ROUTE_HTTP_METHODS, isModernPlanMigrationDiff, normalizeDeployResolveRequest, } from "./deploy.types.js";
|
|
27
|
-
import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isTransferFreezeError, } from "../errors.js";
|
|
27
|
+
import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isRun402Error, isTransferFreezeError, } from "../errors.js";
|
|
28
28
|
import { assertAssetMetadata, assertExifPolicy, } from "./assets-validation.js";
|
|
29
29
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
30
30
|
const PLAN_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
@@ -36,6 +36,10 @@ const APPLY_RETRY_DEFAULT_MAX_RETRIES = 2;
|
|
|
36
36
|
const APPLY_RETRY_BASE_DELAY_MS = 250;
|
|
37
37
|
const APPLY_RETRY_MAX_DELAY_MS = 2_000;
|
|
38
38
|
const APPLY_RETRY_JITTER_MS = 100;
|
|
39
|
+
const CONTENT_COMMIT_MAX_ATTEMPTS = 3;
|
|
40
|
+
const DEPLOY_COMMIT_MAX_ATTEMPTS = 3;
|
|
41
|
+
const DEPLOY_COMMIT_RETRY_BASE_DELAY_MS = 1_000;
|
|
42
|
+
const CONTENT_COMMIT_RETRY_BASE_DELAY_MS = 500;
|
|
39
43
|
const URL_REFRESH_AT_MS = 50 * 60 * 1000;
|
|
40
44
|
const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
41
45
|
const APPLY_SAFE_RETRY_CODES = new Set([
|
|
@@ -568,11 +572,9 @@ async function rehearseBeforeCommit(client, plan, planId, project, opts, emit) {
|
|
|
568
572
|
const envelope = plan.rehearsal;
|
|
569
573
|
if (!envelope || !envelope.available) {
|
|
570
574
|
const reason = envelope?.reason ?? "no_migrations";
|
|
571
|
-
return skip(reason === "no_live_release"
|
|
572
|
-
?
|
|
573
|
-
:
|
|
574
|
-
? "migrations_unchanged"
|
|
575
|
-
: "no_migrations");
|
|
575
|
+
return skip(reason === "no_live_release" || reason === "migrations_unchanged" || reason === "ci_session_unsupported"
|
|
576
|
+
? reason
|
|
577
|
+
: "no_migrations");
|
|
576
578
|
}
|
|
577
579
|
// Named migrations that are all checksum-identical noops (already applied
|
|
578
580
|
// to the live project) would run NOTHING on the branch, so a rehearsal
|
|
@@ -731,7 +733,7 @@ async function applyOnce(client, spec, opts, emit) {
|
|
|
731
733
|
apply_plan_sha256: plan.vault?.apply_plan_sha256 ?? null,
|
|
732
734
|
})
|
|
733
735
|
: undefined;
|
|
734
|
-
const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, requiredPlan, vaultCommit), "applying deploy");
|
|
736
|
+
const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, requiredPlan, vaultCommit, operationId), "applying deploy");
|
|
735
737
|
const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
|
|
736
738
|
result.rehearsal = rehearsal.block;
|
|
737
739
|
// v1.48 unified-apply: thread the plan response's `asset_entries[]` back
|
|
@@ -1591,7 +1593,7 @@ function expandCronNumberPart(part, min, max) {
|
|
|
1591
1593
|
}
|
|
1592
1594
|
return values;
|
|
1593
1595
|
}
|
|
1594
|
-
async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, vault) {
|
|
1596
|
+
async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, vault, operationId) {
|
|
1595
1597
|
try {
|
|
1596
1598
|
const body = {};
|
|
1597
1599
|
if (idempotencyKey)
|
|
@@ -1600,7 +1602,7 @@ async function commitInternal(client, planId, idempotencyKey, project, requiredP
|
|
|
1600
1602
|
body.required_plan = requiredPlanToWire(requiredPlan);
|
|
1601
1603
|
if (vault)
|
|
1602
1604
|
body.vault = vault;
|
|
1603
|
-
|
|
1605
|
+
const send = () => client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/commit`, {
|
|
1604
1606
|
method: "POST",
|
|
1605
1607
|
body,
|
|
1606
1608
|
// Write-approval scope: committing a deploy is `project.deploy` on this project.
|
|
@@ -1615,11 +1617,43 @@ async function commitInternal(client, planId, idempotencyKey, project, requiredP
|
|
|
1615
1617
|
: {}),
|
|
1616
1618
|
context: "committing deploy",
|
|
1617
1619
|
});
|
|
1620
|
+
return await sendCommitRecoveringLostResponse(client, send, operationId, project);
|
|
1618
1621
|
}
|
|
1619
1622
|
catch (err) {
|
|
1620
1623
|
throw translateDeployError(err, "commit", planId, null);
|
|
1621
1624
|
}
|
|
1622
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Send a deploy commit, recovering when a 502/503/504 or a dropped connection
|
|
1628
|
+
* loses the response. The commit may still have reached the gateway (a long
|
|
1629
|
+
* commit can outlive the connection that carried it), and a second commit
|
|
1630
|
+
* while the first is running would drive the state machine twice. So read the
|
|
1631
|
+
* operation first: once it has left `planning`/`uploading` the commit landed,
|
|
1632
|
+
* and the caller follows the operation; otherwise the commit never arrived and
|
|
1633
|
+
* is sent again. Without an operation id to read, the failure surfaces as is.
|
|
1634
|
+
*/
|
|
1635
|
+
async function sendCommitRecoveringLostResponse(client, send, operationId, projectId) {
|
|
1636
|
+
for (let attempt = 1;; attempt++) {
|
|
1637
|
+
try {
|
|
1638
|
+
return await send();
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
if (!operationId || !projectId || !isTransientGatewayFailure(err) || attempt >= DEPLOY_COMMIT_MAX_ATTEMPTS)
|
|
1642
|
+
throw err;
|
|
1643
|
+
await sleep(DEPLOY_COMMIT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
1644
|
+
let snapshot;
|
|
1645
|
+
try {
|
|
1646
|
+
snapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}`, { headers: await apikeyHeaders(client, projectId), context: "fetching deploy operation" });
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
throw err;
|
|
1650
|
+
}
|
|
1651
|
+
if (snapshot.status !== "planning" && snapshot.status !== "uploading") {
|
|
1652
|
+
return { operation_id: operationId, status: "running" };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1623
1657
|
async function uploadMissing(client, projectId, presence, byteReaders, emit) {
|
|
1624
1658
|
// Surface CAS dedup hits so agents can distinguish "N files were already
|
|
1625
1659
|
// present" from "nothing happened". The gateway reports both present and
|
|
@@ -1692,7 +1726,7 @@ async function uploadMissing(client, projectId, presence, byteReaders, emit) {
|
|
|
1692
1726
|
// Plan-level finalize — marks the plan committed in the deploy_plans
|
|
1693
1727
|
// table. Per-session promotion to CAS already happened in the loop
|
|
1694
1728
|
// above; this call is the plan-level idempotency anchor.
|
|
1695
|
-
await client
|
|
1729
|
+
await commitContentPlanWithRetry(client, planRes.plan_id, headers);
|
|
1696
1730
|
}
|
|
1697
1731
|
async function uploadCoreContent(client, projectId, byteReaders, emit) {
|
|
1698
1732
|
const entries = [...byteReaders.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -1721,6 +1755,35 @@ async function uploadCoreContent(client, projectId, byteReaders, emit) {
|
|
|
1721
1755
|
});
|
|
1722
1756
|
}
|
|
1723
1757
|
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Finalize a content plan, riding out a transient gateway failure. A 502/503/504
|
|
1760
|
+
* or a dropped connection means the load balancer or the network lost the
|
|
1761
|
+
* request, not that the gateway refused it, and the commit is safe to repeat: the
|
|
1762
|
+
* gateway re-promotes the same staged sessions. If a lost attempt had in fact
|
|
1763
|
+
* committed, the repeat answers 409 PLAN_ALREADY_COMMITTED, which is success.
|
|
1764
|
+
*/
|
|
1765
|
+
async function commitContentPlanWithRetry(client, planId, headers) {
|
|
1766
|
+
for (let attempt = 1;; attempt++) {
|
|
1767
|
+
try {
|
|
1768
|
+
await client.request(`/content/v1/plans/${encodeURIComponent(planId)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
catch (err) {
|
|
1772
|
+
if (attempt > 1 && isRun402Error(err) && err.status === 409 && err.code === "PLAN_ALREADY_COMMITTED")
|
|
1773
|
+
return;
|
|
1774
|
+
if (!isTransientGatewayFailure(err) || attempt >= CONTENT_COMMIT_MAX_ATTEMPTS)
|
|
1775
|
+
throw err;
|
|
1776
|
+
await sleep(CONTENT_COMMIT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function isTransientGatewayFailure(err) {
|
|
1781
|
+
if (!isRun402Error(err))
|
|
1782
|
+
return false;
|
|
1783
|
+
if (err.kind === "network_error")
|
|
1784
|
+
return true;
|
|
1785
|
+
return err.status === 502 || err.status === 503 || err.status === 504;
|
|
1786
|
+
}
|
|
1724
1787
|
async function uploadOneWithRetry(fetchFn, session, bytes) {
|
|
1725
1788
|
const MAX_ATTEMPTS = 3;
|
|
1726
1789
|
for (let attempt = 1;; attempt++) {
|
|
@@ -2100,8 +2163,8 @@ async function startInternal(client, spec, opts) {
|
|
|
2100
2163
|
status: "started",
|
|
2101
2164
|
...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
|
|
2102
2165
|
});
|
|
2103
|
-
const { planId } = requirePersistedPlan(plan, "starting deploy");
|
|
2104
|
-
const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan), "starting deploy");
|
|
2166
|
+
const { planId, operationId } = requirePersistedPlan(plan, "starting deploy");
|
|
2167
|
+
const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan, undefined, operationId), "starting deploy");
|
|
2105
2168
|
return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
|
|
2106
2169
|
})();
|
|
2107
2170
|
// Avoid an unhandled-rejection at construction time. Consumers must call
|
|
@@ -4034,7 +4097,7 @@ async function uploadInlineCas(client, projectId, bytes, contentType) {
|
|
|
4034
4097
|
await uploadOne(client.fetch, session, bytes);
|
|
4035
4098
|
// v1.48 unified-apply: per-session /storage/v1/uploads/:id/complete is
|
|
4036
4099
|
// gone (404). The plan-level commit below promotes the session to CAS.
|
|
4037
|
-
await client
|
|
4100
|
+
await commitContentPlanWithRetry(client, planRes.plan_id, headers);
|
|
4038
4101
|
}
|
|
4039
4102
|
return { sha256, size: bytes.byteLength, contentType };
|
|
4040
4103
|
}
|