From 2de68342b5fba07efb3a6e3435d7963dabf0530a Mon Sep 17 00:00:00 2001 From: Ethan Byrd Date: Mon, 11 May 2026 10:58:27 -0700 Subject: [PATCH 1/2] Bump @primitivedotdev/cli to 0.25.2 Ships --secret KEY=VALUE on functions:deploy and functions:redeploy, and the scaffolded handler's self-reply guard + REPLY_FROM constant. --- cli-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli-node/package.json b/cli-node/package.json index 527ca48..d2f6d7c 100644 --- a/cli-node/package.json +++ b/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@primitivedotdev/cli", - "version": "0.25.1", + "version": "0.25.2", "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.", "type": "module", "sideEffects": false, From caaba9dbbc801c5761a7805b61f173eff821997c Mon Sep 17 00:00:00 2001 From: Ethan Byrd Date: Mon, 11 May 2026 11:02:17 -0700 Subject: [PATCH 2/2] Surface un-attempted keys when set-secret fails mid-deploy Greptile follow-up: the set-secret failure hint named only the failed key, so a user re-running set-secret for that key alone would still leave any keys that came after it un-written. Track pendingKeys in the orchestrator result and list them in the stderr hint. --- .../src/oclif/commands/functions-deploy.ts | 22 +++++++++++++++++-- .../src/oclif/commands/functions-redeploy.ts | 19 ++++++++++++++-- cli-node/tests/oclif/functions-deploy.test.ts | 5 +++++ .../tests/oclif/functions-redeploy.test.ts | 3 +++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cli-node/src/oclif/commands/functions-deploy.ts b/cli-node/src/oclif/commands/functions-deploy.ts index 9adc10d..3296fc4 100644 --- a/cli-node/src/oclif/commands/functions-deploy.ts +++ b/cli-node/src/oclif/commands/functions-deploy.ts @@ -111,6 +111,7 @@ export type RunDeployWithSecretsResult = created: CreateFunctionResult; succeededKeys: string[]; failedKey: string; + pendingKeys: string[]; } | { kind: "error"; @@ -169,7 +170,15 @@ export async function runDeployWithSecrets( const writtenSecrets: FunctionSecretWriteResult[] = []; const succeededKeys: string[] = []; - for (const pair of params.secrets) { + for (let i = 0; i < params.secrets.length; i++) { + const pair = params.secrets[i]; + // Pre-compute the keys that come AFTER the current pair so a + // set-secret failure can surface every key that was never + // attempted, not just the one that failed. Without this, a user + // following the recovery hint verbatim would re-run set-secret + // only for the failed key and silently leave the trailing keys + // un-written. + const pendingKeys = params.secrets.slice(i + 1).map((p) => p.key); const setResult = await api.setSecret({ id: created.id, key: pair.key, @@ -181,6 +190,7 @@ export async function runDeployWithSecrets( failedKey: pair.key, kind: "error", payload: extractErrorPayload(setResult.error), + pendingKeys, stage: "set-secret", succeededKeys, }; @@ -195,6 +205,7 @@ export async function runDeployWithSecrets( code: "client_error", message: "Secret write returned no data", }, + pendingKeys, stage: "set-secret", succeededKeys, }; @@ -412,8 +423,15 @@ class FunctionsDeployCommand extends Command { outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)"; + const pending = + outcome.pendingKeys.length > 0 + ? outcome.pendingKeys.join(", ") + : "(none)"; + const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join( + ", ", + ); process.stderr.write( - `Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}. The redeploy is NOT yet live. Re-run \`primitive functions:set-secret --id ${outcome.created.id} --key ${outcome.failedKey} --value --redeploy\` after fixing the cause.\n`, + `Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${outcome.created.id} --file \` to push them live.\n`, ); } else if (outcome.stage === "redeploy") { const succeeded = diff --git a/cli-node/src/oclif/commands/functions-redeploy.ts b/cli-node/src/oclif/commands/functions-redeploy.ts index e8efcef..bcdc301 100644 --- a/cli-node/src/oclif/commands/functions-redeploy.ts +++ b/cli-node/src/oclif/commands/functions-redeploy.ts @@ -76,6 +76,7 @@ export type RunRedeployWithSecretsResult = payload: unknown; succeededKeys: string[]; failedKey: string; + pendingKeys: string[]; } | { kind: "error"; @@ -101,7 +102,12 @@ export async function runRedeployWithSecrets( ): Promise { const writtenSecrets: FunctionSecretWriteResult[] = []; const succeededKeys: string[] = []; - for (const pair of params.secrets) { + for (let i = 0; i < params.secrets.length; i++) { + const pair = params.secrets[i]; + // Pre-compute the keys that come AFTER the current pair so a + // set-secret failure can surface every key that was never + // attempted, not just the one that failed. + const pendingKeys = params.secrets.slice(i + 1).map((p) => p.key); const setResult = await api.setSecret({ id: params.id, key: pair.key, @@ -112,6 +118,7 @@ export async function runRedeployWithSecrets( failedKey: pair.key, kind: "error", payload: extractErrorPayload(setResult.error), + pendingKeys, stage: "set-secret", succeededKeys, }; @@ -125,6 +132,7 @@ export async function runRedeployWithSecrets( code: "client_error", message: "Secret write returned no data", }, + pendingKeys, stage: "set-secret", succeededKeys, }; @@ -319,8 +327,15 @@ class FunctionsRedeployCommand extends Command { outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)"; + const pending = + outcome.pendingKeys.length > 0 + ? outcome.pendingKeys.join(", ") + : "(none)"; + const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join( + ", ", + ); process.stderr.write( - `Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}. The new bundle has NOT been deployed. Re-run \`primitive functions:set-secret --id ${flags.id} --key ${outcome.failedKey} --value \` after fixing the cause, then \`primitive functions:redeploy --id ${flags.id} --file \`.\n`, + `Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions:set-secret\` for each of [${allMissing}], then \`primitive functions:redeploy --id ${flags.id} --file \` to push them live.\n`, ); } else if (outcome.stage === "redeploy") { const succeeded = diff --git a/cli-node/tests/oclif/functions-deploy.test.ts b/cli-node/tests/oclif/functions-deploy.test.ts index 1d9ba29..9f99023 100644 --- a/cli-node/tests/oclif/functions-deploy.test.ts +++ b/cli-node/tests/oclif/functions-deploy.test.ts @@ -394,6 +394,11 @@ describe("runDeployWithSecrets (--secret K=V)", () => { if (outcome.kind === "error" && outcome.stage === "set-secret") { expect(outcome.failedKey).toBe("SECOND"); expect(outcome.succeededKeys).toEqual(["FIRST"]); + // pendingKeys must include every key after the failure so the + // CLI hint can list keys the user still needs to set; otherwise + // re-running set-secret only for failedKey silently leaves THIRD + // un-written. + expect(outcome.pendingKeys).toEqual(["THIRD"]); expect(outcome.created.id).toBe(FN_ID); expect(outcome.payload).toEqual({ code: "validation", diff --git a/cli-node/tests/oclif/functions-redeploy.test.ts b/cli-node/tests/oclif/functions-redeploy.test.ts index f197d82..5b67544 100644 --- a/cli-node/tests/oclif/functions-redeploy.test.ts +++ b/cli-node/tests/oclif/functions-redeploy.test.ts @@ -217,6 +217,9 @@ describe("runRedeployWithSecrets (--secret K=V)", () => { if (outcome.kind === "error" && outcome.stage === "set-secret") { expect(outcome.failedKey).toBe("SECOND"); expect(outcome.succeededKeys).toEqual(["FIRST"]); + // pendingKeys lists keys never attempted so the CLI hint can + // direct the user to re-set them too, not just the failed one. + expect(outcome.pendingKeys).toEqual(["THIRD"]); expect(outcome.payload).toEqual({ code: "validation", message: "value too long",