Skip to content

Commit 816ca6d

Browse files
Copilotvladfrangu
andcommitted
Add --ignore-missing-secrets flag to push and run commands with comprehensive test coverage
Co-authored-by: vladfrangu <17960496+vladfrangu@users.noreply.github.com>
1 parent 0162b60 commit 816ca6d

File tree

4 files changed

+149
-12
lines changed

4 files changed

+149
-12
lines changed

src/commands/actors/push.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export class ActorsPushCommand extends ApifyCommand<typeof ActorsPushCommand> {
7979
description: 'Directory where the Actor is located',
8080
required: false,
8181
}),
82+
'ignore-missing-secrets': Flags.boolean({
83+
description: 'Ignore missing secrets and show warnings instead of failing. Environment variables referencing missing secrets will be omitted.',
84+
default: false,
85+
required: false,
86+
}),
8287
};
8388

8489
static override args = {
@@ -280,7 +285,11 @@ Skipping push. Use --force to override.`,
280285
// Update Actor version
281286
const actorCurrentVersion = await actorClient.version(version).get();
282287
const envVars = actorConfig!.environmentVariables
283-
? transformEnvToEnvVars(actorConfig!.environmentVariables as Record<string, string>)
288+
? transformEnvToEnvVars(
289+
actorConfig!.environmentVariables as Record<string, string>,
290+
undefined,
291+
this.flags.ignoreMissingSecrets
292+
)
284293
: undefined;
285294

286295
if (actorCurrentVersion) {

src/commands/run.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export class RunCommand extends ApifyCommand<typeof RunCommand> {
9898
stdin: StdinMode.Stringified,
9999
exclusive: ['input'],
100100
}),
101+
'ignore-missing-secrets': Flags.boolean({
102+
description: 'Ignore missing secrets and show warnings instead of failing. Environment variables referencing missing secrets will be omitted.',
103+
default: false,
104+
required: false,
105+
}),
101106
};
102107

103108
async run() {
@@ -259,7 +264,11 @@ export class RunCommand extends ApifyCommand<typeof RunCommand> {
259264
if (userId) localEnvVars[APIFY_ENV_VARS.USER_ID] = userId;
260265
if (token) localEnvVars[APIFY_ENV_VARS.TOKEN] = token;
261266
if (localConfig!.environmentVariables) {
262-
const updatedEnv = replaceSecretsValue(localConfig!.environmentVariables as Record<string, string>);
267+
const updatedEnv = replaceSecretsValue(
268+
localConfig!.environmentVariables as Record<string, string>,
269+
undefined,
270+
this.flags.ignoreMissingSecrets
271+
);
263272
Object.assign(localEnvVars, updatedEnv);
264273
}
265274

src/lib/secrets.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync, writeFileSync } from 'node:fs';
22

33
import { SECRETS_FILE_PATH } from './consts.js';
4+
import { warning } from './outputs.js';
45
import { ensureApifyDirectory } from './utils.js';
56

67
const SECRET_KEY_PREFIX = '@';
@@ -53,8 +54,9 @@ const isSecretKey = (envValue: string) => {
5354
* Replaces secure values in env with proper values from local secrets file.
5455
* @param env
5556
* @param secrets - Object with secrets, if not set, will be load from secrets file.
57+
* @param ignoreMissingSecrets - If true, emit warnings for missing secrets instead of throwing errors
5658
*/
57-
export const replaceSecretsValue = (env: Record<string, string>, secrets?: Record<string, string>) => {
59+
export const replaceSecretsValue = (env: Record<string, string>, secrets?: Record<string, string>, ignoreMissingSecrets?: boolean) => {
5860
secrets = secrets || getSecretsFile();
5961
const updatedEnv = {};
6062
const missingSecrets: string[] = [];
@@ -75,10 +77,19 @@ export const replaceSecretsValue = (env: Record<string, string>, secrets?: Recor
7577
});
7678

7779
if (missingSecrets.length > 0) {
78-
throw new Error(
79-
`Missing secrets: ${missingSecrets.join(', ')}. ` +
80-
`Set them by calling "apify secrets add <SECRET_NAME> <SECRET_VALUE>".`
81-
);
80+
if (ignoreMissingSecrets) {
81+
// Emit warnings for each missing secret, keeping original behavior
82+
missingSecrets.forEach(secretKey => {
83+
warning({
84+
message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`,
85+
});
86+
});
87+
} else {
88+
throw new Error(
89+
`Missing secrets: ${missingSecrets.join(', ')}. ` +
90+
`Set them by calling "apify secrets add <SECRET_NAME> <SECRET_VALUE>".`
91+
);
92+
}
8293
}
8394

8495
return updatedEnv;
@@ -93,9 +104,11 @@ interface EnvVar {
93104
/**
94105
* Transform env to envVars format attribute, which uses Apify API
95106
* It replaces secrets to values from secrets file.
107+
* @param env
96108
* @param secrets - Object with secrets, if not set, will be load from secrets file.
109+
* @param ignoreMissingSecrets - If true, emit warnings for missing secrets instead of throwing errors
97110
*/
98-
export const transformEnvToEnvVars = (env: Record<string, string>, secrets?: Record<string, string>) => {
111+
export const transformEnvToEnvVars = (env: Record<string, string>, secrets?: Record<string, string>, ignoreMissingSecrets?: boolean) => {
99112
secrets = secrets || getSecretsFile();
100113
const envVars: EnvVar[] = [];
101114
const missingSecrets: string[] = [];
@@ -121,10 +134,19 @@ export const transformEnvToEnvVars = (env: Record<string, string>, secrets?: Rec
121134
});
122135

123136
if (missingSecrets.length > 0) {
124-
throw new Error(
125-
`Missing secrets: ${missingSecrets.join(', ')}. ` +
126-
`Set them by calling "apify secrets add <SECRET_NAME> <SECRET_VALUE>".`
127-
);
137+
if (ignoreMissingSecrets) {
138+
// Emit warnings for each missing secret, keeping original behavior
139+
missingSecrets.forEach(secretKey => {
140+
warning({
141+
message: `Value for ${secretKey} not found in local secrets. Set it by calling "apify secrets add ${secretKey} [SECRET_VALUE]"`,
142+
});
143+
});
144+
} else {
145+
throw new Error(
146+
`Missing secrets: ${missingSecrets.join(', ')}. ` +
147+
`Set them by calling "apify secrets add <SECRET_NAME> <SECRET_VALUE>".`
148+
);
149+
}
128150
}
129151

130152
return envVars;

test/local/lib/secrets.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,101 @@ describe('Secrets', () => {
160160
expect(result.map(r => r.name)).toEqual(['THIRD', 'FIRST', 'SECOND']);
161161
});
162162
});
163+
164+
describe('ignoreMissingSecrets flag behavior', () => {
165+
it('transformEnvToEnvVars should emit warnings when ignoreMissingSecrets is true', () => {
166+
const spy = vitest.spyOn(console, 'error');
167+
168+
const secrets = {
169+
validSecret: 'validValue',
170+
};
171+
const env = {
172+
VALID_SECRET: '@validSecret',
173+
INVALID_SECRET: '@missingSecret',
174+
NORMAL_VAR: 'normalValue',
175+
};
176+
177+
const result = transformEnvToEnvVars(env, secrets, true);
178+
179+
// Should include valid secret and normal var, but omit missing secret
180+
expect(result).toEqual([
181+
{
182+
name: 'VALID_SECRET',
183+
value: 'validValue',
184+
isSecret: true,
185+
},
186+
{
187+
name: 'NORMAL_VAR',
188+
value: 'normalValue',
189+
},
190+
]);
191+
192+
// Should have emitted a warning
193+
expect(spy).toHaveBeenCalled();
194+
expect(spy.mock.calls[0][0]).to.include('Warning:');
195+
});
196+
197+
it('replaceSecretsValue should emit warnings when ignoreMissingSecrets is true', () => {
198+
const spy = vitest.spyOn(console, 'error');
199+
200+
const secrets = {
201+
validSecret: 'validValue',
202+
};
203+
const env = {
204+
VALID_SECRET: '@validSecret',
205+
INVALID_SECRET: '@missingSecret',
206+
NORMAL_VAR: 'normalValue',
207+
};
208+
209+
const result = replaceSecretsValue(env, secrets, true);
210+
211+
// Should include valid secret and normal var, but omit missing secret
212+
expect(result).toEqual({
213+
VALID_SECRET: 'validValue',
214+
NORMAL_VAR: 'normalValue',
215+
});
216+
217+
// Should have emitted a warning
218+
expect(spy).toHaveBeenCalled();
219+
expect(spy.mock.calls[0][0]).to.include('Warning:');
220+
});
221+
222+
it('should still throw error when ignoreMissingSecrets is false (default behavior)', () => {
223+
const secrets = {};
224+
const env = {
225+
INVALID_SECRET: '@missingSecret',
226+
};
227+
228+
// Should throw when ignoreMissingSecrets is false (default)
229+
expect(() => transformEnvToEnvVars(env, secrets, false)).toThrow('Missing secrets: missingSecret');
230+
231+
// Should also throw when parameter is not provided (default)
232+
expect(() => transformEnvToEnvVars(env, secrets)).toThrow('Missing secrets: missingSecret');
233+
});
234+
235+
it('should handle multiple missing secrets with ignoreMissingSecrets', () => {
236+
const spy = vitest.spyOn(console, 'error');
237+
238+
const secrets = {};
239+
const env = {
240+
SECRET1: '@missing1',
241+
SECRET2: '@missing2',
242+
NORMAL_VAR: 'value',
243+
};
244+
245+
const result = transformEnvToEnvVars(env, secrets, true);
246+
247+
expect(result).toEqual([
248+
{
249+
name: 'NORMAL_VAR',
250+
value: 'value',
251+
},
252+
]);
253+
254+
// Should have emitted warnings for both missing secrets
255+
expect(spy).toHaveBeenCalledTimes(2);
256+
expect(spy.mock.calls[0][0]).to.include('Warning:');
257+
expect(spy.mock.calls[1][0]).to.include('Warning:');
258+
});
259+
});
163260
});

0 commit comments

Comments
 (0)