From 9156844052cd23b4bc4e6d2b047ab7a591de2ae2 Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 12 Mar 2026 11:26:20 +0100 Subject: [PATCH 1/2] fix: Throw an error on missing secrets --- src/lib/secrets.ts | 31 +++++++++++++++----- test/local/lib/secrets.test.ts | 53 +++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/lib/secrets.ts b/src/lib/secrets.ts index a373d8470..6416cbaa3 100644 --- a/src/lib/secrets.ts +++ b/src/lib/secrets.ts @@ -1,7 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { SECRETS_FILE_PATH } from './consts.js'; -import { warning } from './outputs.js'; import { ensureApifyDirectory } from './utils.js'; const SECRET_KEY_PREFIX = '@'; @@ -58,6 +57,8 @@ const isSecretKey = (envValue: string) => { export const replaceSecretsValue = (env: Record, secrets?: Record) => { secrets = secrets || getSecretsFile(); const updatedEnv = {}; + const missingSecrets: string[] = []; + Object.keys(env).forEach((key) => { if (isSecretKey(env[key])) { const secretKey = env[key].replace(new RegExp(`^${SECRET_KEY_PREFIX}`), ''); @@ -65,15 +66,22 @@ export const replaceSecretsValue = (env: Record, secrets?: Recor // @ts-expect-error - we are replacing the value updatedEnv[key] = secrets[secretKey]; } else { - warning({ - message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`, - }); + missingSecrets.push(secretKey); } } else { // @ts-expect-error - we are replacing the value updatedEnv[key] = env[key]; } }); + + if (missingSecrets.length > 0) { + const secretsList = missingSecrets.map((s) => ` - ${s}`).join('\n'); + throw new Error( + `The following secrets are missing:\n${secretsList}\n\n` + + `Set them by calling "apify secrets add " for each missing secret.`, + ); + } + return updatedEnv; }; @@ -91,6 +99,8 @@ interface EnvVar { export const transformEnvToEnvVars = (env: Record, secrets?: Record) => { secrets = secrets || getSecretsFile(); const envVars: EnvVar[] = []; + const missingSecrets: string[] = []; + Object.keys(env).forEach((key) => { if (isSecretKey(env[key])) { const secretKey = env[key].replace(new RegExp(`^${SECRET_KEY_PREFIX}`), ''); @@ -101,9 +111,7 @@ export const transformEnvToEnvVars = (env: Record, secrets?: Rec isSecret: true, }); } else { - warning({ - message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`, - }); + missingSecrets.push(secretKey); } } else { envVars.push({ @@ -112,5 +120,14 @@ export const transformEnvToEnvVars = (env: Record, secrets?: Rec }); } }); + + if (missingSecrets.length > 0) { + const secretsList = missingSecrets.map((s) => ` - ${s}`).join('\n'); + throw new Error( + `The following secrets are missing:\n${secretsList}\n\n` + + `Set them by calling "apify secrets add " for each missing secret.`, + ); + } + return envVars; }; diff --git a/test/local/lib/secrets.test.ts b/test/local/lib/secrets.test.ts index cbfea832f..9359a0928 100644 --- a/test/local/lib/secrets.test.ts +++ b/test/local/lib/secrets.test.ts @@ -1,10 +1,8 @@ -import { replaceSecretsValue } from '../../../src/lib/secrets.js'; +import { replaceSecretsValue, transformEnvToEnvVars } from '../../../src/lib/secrets.js'; describe('Secrets', () => { describe('replaceSecretsValue()', () => { - it('should work', () => { - const spy = vitest.spyOn(console, 'error'); - + it('should replace secret references with their values', () => { const secrets = { myProdToken: 'mySecretToken', mongoUrl: 'mongo://bla@bla:supermongo.com:27017', @@ -13,7 +11,6 @@ describe('Secrets', () => { TOKEN: '@myProdToken', USER: 'jakub.drobnik@apify.com', MONGO_URL: '@mongoUrl', - WARNING: '@doesNotExist', }; const updatedEnv = replaceSecretsValue(env, secrets); @@ -22,9 +19,51 @@ describe('Secrets', () => { USER: 'jakub.drobnik@apify.com', MONGO_URL: secrets.mongoUrl, }); + }); + + it('should throw an error when secrets are missing', () => { + const secrets = { + myProdToken: 'mySecretToken', + }; + const env = { + TOKEN: '@myProdToken', + MISSING_ONE: '@doesNotExist', + MISSING_TWO: '@alsoMissing', + }; + + expect(() => replaceSecretsValue(env, secrets)).toThrow( + /The following secrets are missing:\n\s+- doesNotExist\n\s+- alsoMissing/, + ); + }); + }); + + describe('transformEnvToEnvVars()', () => { + it('should transform env to envVars format with secret resolution', () => { + const secrets = { + myProdToken: 'mySecretToken', + }; + const env = { + TOKEN: '@myProdToken', + USER: 'jakub.drobnik@apify.com', + }; + const envVars = transformEnvToEnvVars(env, secrets); + + expect(envVars).toStrictEqual([ + { name: 'TOKEN', value: 'mySecretToken', isSecret: true }, + { name: 'USER', value: 'jakub.drobnik@apify.com' }, + ]); + }); + + it('should throw an error when secrets are missing', () => { + const secrets = {}; + const env = { + TOKEN: '@doesNotExist', + USER: 'plain-value', + }; - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).to.include('Warning:'); + expect(() => transformEnvToEnvVars(env, secrets)).toThrow( + /The following secrets are missing:\n\s+- doesNotExist/, + ); }); }); }); From f5f7b71a4e13ad0dc633d475b18c83ae300443a5 Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Fri, 13 Mar 2026 18:24:14 +0100 Subject: [PATCH 2/2] Add --allow-missing-secrets flag --- src/commands/actors/push.ts | 10 +++++- src/commands/run.ts | 12 ++++++- src/lib/secrets.ts | 47 +++++++++++++++++++++------ test/local/lib/secrets.test.ts | 59 ++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 3c3073a4f..49b17daa5 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -83,6 +83,12 @@ export class ActorsPushCommand extends ApifyCommand { description: 'Directory where the Actor is located', required: false, }), + 'allow-missing-secrets': Flags.boolean({ + description: + 'Allow the command to continue even when secret values are not found in the local secrets storage.', + required: false, + default: false, + }), }; static override args = { @@ -293,7 +299,9 @@ Skipping push. Use --force to override.`, // Update Actor version const actorCurrentVersion = await actorClient.version(version).get(); const envVars = actorConfig!.environmentVariables - ? transformEnvToEnvVars(actorConfig!.environmentVariables as Record) + ? transformEnvToEnvVars(actorConfig!.environmentVariables as Record, undefined, { + allowMissing: this.flags.allowMissingSecrets, + }) : undefined; if (actorCurrentVersion) { diff --git a/src/commands/run.ts b/src/commands/run.ts index cc1dbaf26..917f9593f 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -98,6 +98,12 @@ export class RunCommand extends ApifyCommand { stdin: StdinMode.Stringified, exclusive: ['input'], }), + 'allow-missing-secrets': Flags.boolean({ + description: + 'Allow the command to continue even when secret values are not found in the local secrets storage.', + required: false, + default: false, + }), }; async run() { @@ -265,7 +271,11 @@ export class RunCommand extends ApifyCommand { if (userId) localEnvVars[APIFY_ENV_VARS.USER_ID] = userId; if (token) localEnvVars[APIFY_ENV_VARS.TOKEN] = token; if (localConfig!.environmentVariables) { - const updatedEnv = replaceSecretsValue(localConfig!.environmentVariables as Record); + const updatedEnv = replaceSecretsValue( + localConfig!.environmentVariables as Record, + undefined, + { allowMissing: this.flags.allowMissingSecrets }, + ); Object.assign(localEnvVars, updatedEnv); } diff --git a/src/lib/secrets.ts b/src/lib/secrets.ts index 6416cbaa3..ed1a35fe6 100644 --- a/src/lib/secrets.ts +++ b/src/lib/secrets.ts @@ -1,6 +1,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { SECRETS_FILE_PATH } from './consts.js'; +import { warning } from './outputs.js'; import { ensureApifyDirectory } from './utils.js'; const SECRET_KEY_PREFIX = '@'; @@ -54,7 +55,11 @@ const isSecretKey = (envValue: string) => { * @param env * @param secrets - Object with secrets, if not set, will be load from secrets file. */ -export const replaceSecretsValue = (env: Record, secrets?: Record) => { +export const replaceSecretsValue = ( + env: Record, + secrets?: Record, + { allowMissing = false }: { allowMissing?: boolean } = {}, +) => { secrets = secrets || getSecretsFile(); const updatedEnv = {}; const missingSecrets: string[] = []; @@ -76,10 +81,19 @@ export const replaceSecretsValue = (env: Record, secrets?: Recor if (missingSecrets.length > 0) { const secretsList = missingSecrets.map((s) => ` - ${s}`).join('\n'); - throw new Error( - `The following secrets are missing:\n${secretsList}\n\n` + - `Set them by calling "apify secrets add " for each missing secret.`, - ); + if (allowMissing) { + for (const secretKey of missingSecrets) { + warning({ + message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`, + }); + } + } else { + throw new Error( + `The following secrets are missing:\n${secretsList}\n\n` + + `Set them by calling "apify secrets add " for each missing secret.\n` + + `If you want to skip missing secrets, run the command with the --allow-missing-secrets flag.`, + ); + } } return updatedEnv; @@ -96,7 +110,11 @@ interface EnvVar { * It replaces secrets to values from secrets file. * @param secrets - Object with secrets, if not set, will be load from secrets file. */ -export const transformEnvToEnvVars = (env: Record, secrets?: Record) => { +export const transformEnvToEnvVars = ( + env: Record, + secrets?: Record, + { allowMissing = false }: { allowMissing?: boolean } = {}, +) => { secrets = secrets || getSecretsFile(); const envVars: EnvVar[] = []; const missingSecrets: string[] = []; @@ -123,10 +141,19 @@ export const transformEnvToEnvVars = (env: Record, secrets?: Rec if (missingSecrets.length > 0) { const secretsList = missingSecrets.map((s) => ` - ${s}`).join('\n'); - throw new Error( - `The following secrets are missing:\n${secretsList}\n\n` + - `Set them by calling "apify secrets add " for each missing secret.`, - ); + if (allowMissing) { + for (const secretKey of missingSecrets) { + warning({ + message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`, + }); + } + } else { + throw new Error( + `The following secrets are missing:\n${secretsList}\n\n` + + `Set them by calling "apify secrets add " for each missing secret.\n` + + `If you want to skip missing secrets, run the command with the --allow-missing-secrets flag.`, + ); + } } return envVars; diff --git a/test/local/lib/secrets.test.ts b/test/local/lib/secrets.test.ts index 9359a0928..55568157e 100644 --- a/test/local/lib/secrets.test.ts +++ b/test/local/lib/secrets.test.ts @@ -35,6 +35,38 @@ describe('Secrets', () => { /The following secrets are missing:\n\s+- doesNotExist\n\s+- alsoMissing/, ); }); + + it('should mention --allow-missing-secrets in the error message', () => { + const env = { TOKEN: '@doesNotExist' }; + + expect(() => replaceSecretsValue(env, {})).toThrow(/--allow-missing-secrets/); + }); + + it('should warn instead of throwing when allowMissing is true', () => { + const spy = vitest.spyOn(console, 'error'); + + const secrets = { + myProdToken: 'mySecretToken', + }; + const env = { + TOKEN: '@myProdToken', + MISSING_ONE: '@doesNotExist', + MISSING_TWO: '@alsoMissing', + }; + + const updatedEnv = replaceSecretsValue(env, secrets, { + allowMissing: true, + }); + + expect(updatedEnv).toStrictEqual({ + TOKEN: secrets.myProdToken, + }); + + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls.flat().join(' ')).to.include('doesNotExist'); + + spy.mockRestore(); + }); }); describe('transformEnvToEnvVars()', () => { @@ -65,5 +97,32 @@ describe('Secrets', () => { /The following secrets are missing:\n\s+- doesNotExist/, ); }); + + it('should mention --allow-missing-secrets in the error message', () => { + const env = { TOKEN: '@doesNotExist' }; + + expect(() => transformEnvToEnvVars(env, {})).toThrow(/--allow-missing-secrets/); + }); + + it('should warn instead of throwing when allowMissing is true', () => { + const spy = vitest.spyOn(console, 'error'); + + const secrets = {}; + const env = { + TOKEN: '@doesNotExist', + USER: 'plain-value', + }; + + const envVars = transformEnvToEnvVars(env, secrets, { + allowMissing: true, + }); + + expect(envVars).toStrictEqual([{ name: 'USER', value: 'plain-value' }]); + + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls.flat().join(' ')).to.include('doesNotExist'); + + spy.mockRestore(); + }); }); });