Skip to content

Commit c7fe62d

Browse files
committed
refactor: replace yargs .env("GCL") with manual env var injection
Removes yargs .env("GCL") which blocked GCL_VARIABLE_* env vars via strictOptions. Env vars are now derived from yargs option metadata, so option names are defined once. Array options split on semicolons, naturally supporting GCL_VARIABLE=A=1;B=2 bulk format. Integration test now uses subprocess with isolated env instead of mutating process.env.
1 parent 353ba40 commit c7fe62d

3 files changed

Lines changed: 61 additions & 39 deletions

File tree

src/argv.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,50 @@ export function injectGclVariableEnvVars (argv: {variable?: string[]}, env: Reco
3535
}
3636
}
3737

38+
interface YargsOptionsMeta {
39+
"array": string[];
40+
"boolean": string[];
41+
"number": string[];
42+
"default": Record<string, any>;
43+
"key": Record<string, boolean>;
44+
}
45+
46+
export function injectGclEnvVars (
47+
argv: Record<string, any>,
48+
yargsOptions: YargsOptionsMeta,
49+
env: Record<string, string | undefined>,
50+
): void {
51+
const arrays = new Set(yargsOptions.array.map(String));
52+
const booleans = new Set(yargsOptions.boolean.map(String));
53+
const numbers = new Set(yargsOptions.number.map(String));
54+
55+
for (const key of Object.keys(yargsOptions.key)) {
56+
if (key.includes("-") || key === "_" || key === "$0") continue;
57+
58+
const envKey = `GCL_${key.replace(/[A-Z]/g, c => `_${c}`).toUpperCase()}`;
59+
const envValue = env[envKey];
60+
if (envValue == null) continue;
61+
62+
if (arrays.has(key)) {
63+
const cliValues = Array.isArray(argv[key]) ? argv[key] : [];
64+
argv[key] = [...envValue.split(";"), ...cliValues];
65+
continue;
66+
}
67+
68+
if (argv[key] !== undefined && argv[key] !== yargsOptions.default[key]) continue;
69+
70+
if (booleans.has(key)) argv[key] = envValue === "true" || envValue === "1";
71+
else if (numbers.has(key)) argv[key] = Number(envValue);
72+
else argv[key] = envValue;
73+
}
74+
}
75+
3876
export class Argv {
3977
static readonly default = {
4078
"variablesFile": ".gitlab-ci-local-variables.yml",
4179
"evaluateRuleChanges": true,
4280
"ignoreSchemaPaths": [],
43-
"ignorePredefinedVars": "",
81+
"ignorePredefinedVars": [] as string[],
4482
};
4583

4684
map: Map<string, any> = new Map<string, any>();
@@ -56,7 +94,6 @@ export class Argv {
5694
}
5795

5896
static async build (args: any, writeStreams?: WriteStreams) {
59-
injectGclVariableEnvVars(args, process.env);
6097
const argv = new Argv(args, writeStreams);
6198
await argv.fallbackCwd(args);
6299

@@ -177,7 +214,9 @@ export class Argv {
177214
}
178215

179216
get ignorePredefinedVars (): string[] {
180-
return this.map.get("ignorePredefinedVars") ?? Argv.default.ignorePredefinedVars;
217+
const val = this.map.get("ignorePredefinedVars") ?? [];
218+
if (!Array.isArray(val)) return val ? String(val).split(",") : [];
219+
return val.flatMap((v: string) => v.split(","));
181220
}
182221

183222
get pullPolicy (): string {

src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as state from "./state.js";
66
import {WriteStreamsProcess, WriteStreamsMock} from "./write-streams.js";
77
import {handler} from "./handler.js";
88
import {Executor} from "./executor.js";
9-
import {Argv} from "./argv.js";
9+
import {Argv, injectGclVariableEnvVars, injectGclEnvVars} from "./argv.js";
1010
import {AssertionError} from "assert";
1111
import {Job, cleanupJobResources} from "./job.js";
1212
import {GitlabRunnerPresetValues} from "./gitlab-preset.js";
@@ -41,6 +41,8 @@ process.on("SIGUSR2", async () => {
4141
.command({
4242
handler: async (argv) => {
4343
try {
44+
injectGclVariableEnvVars(argv, process.env);
45+
injectGclEnvVars(argv, yparser.getOptions(), process.env);
4446
await handler(argv, new WriteStreamsProcess(), jobs);
4547
const failedJobs = Executor.getFailed(jobs);
4648
process.exit(failedJobs.length > 0 ? 1 : 0);
@@ -74,7 +76,6 @@ process.on("SIGUSR2", async () => {
7476
})
7577
.usage("Find more information at https://github.com/firecow/gitlab-ci-local.\nNote: To negate an option use '--no-(option)'.")
7678
.strictOptions()
77-
.env("GCL")
7879
.option("manual", {
7980
type: "array",
8081
description: "One or more manual jobs to run during a pipeline",
@@ -324,11 +325,10 @@ process.on("SIGUSR2", async () => {
324325
description: "The json schema paths that will be ignored",
325326
})
326327
.option("ignore-predefined-vars", {
327-
type: "string",
328-
coerce: (v) => v.split(","),
328+
type: "array",
329329
requiresArg: false,
330330
default: Argv.default.ignorePredefinedVars,
331-
describe: "Comma-seperated list of predefined pipeline variables for which warnings should be suppressed",
331+
describe: "Predefined pipeline variables for which warnings should be suppressed",
332332
})
333333
.option("concurrency", {
334334
type: "number",

tests/test-cases/gcl-variable-env/integration.test.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import {WriteStreamsMock} from "../../../src/write-streams.js";
2-
import {handler} from "../../../src/handler.js";
31
import {injectGclVariableEnvVars} from "../../../src/argv.js";
4-
import chalk from "chalk-template";
5-
import {initSpawnSpy} from "../../mocks/utils.mock.js";
6-
import {WhenStatics} from "../../mocks/when-statics.js";
2+
import {execFile} from "child_process";
3+
import {promisify} from "util";
74

8-
beforeAll(() => {
9-
initSpawnSpy(WhenStatics.all);
10-
});
5+
const execFileAsync = promisify(execFile);
116

127
describe("injectGclVariableEnvVars unit tests", () => {
138
test("injects single GCL_VARIABLE_ entry", () => {
@@ -80,27 +75,15 @@ describe("injectGclVariableEnvVars unit tests", () => {
8075
});
8176
});
8277

83-
describe("injectGclVariableEnvVars integration via process.env", () => {
84-
test.concurrent("GCL_VARIABLE_* env vars are injected into job output", async () => {
85-
const envKeys = ["GCL_VARIABLE_MY_VAR", "GCL_VARIABLE_ANOTHER_VAR"];
86-
process.env["GCL_VARIABLE_MY_VAR"] = "hello";
87-
process.env["GCL_VARIABLE_ANOTHER_VAR"] = "world";
88-
try {
89-
const writeStreams = new WriteStreamsMock();
90-
await handler({
91-
cwd: "tests/test-cases/gcl-variable-env",
92-
job: ["test-job"],
93-
}, writeStreams);
94-
95-
const expected = [
96-
chalk`{blueBright test-job} {greenBright >} hello`,
97-
chalk`{blueBright test-job} {greenBright >} world`,
98-
];
99-
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
100-
} finally {
101-
for (const key of envKeys) {
102-
delete process.env[key];
103-
}
104-
}
78+
test("GCL_VARIABLE_* env vars are injected into job output via CLI", async () => {
79+
const {stdout} = await execFileAsync("bun", ["src/index.ts", "test-job", "--cwd", "tests/test-cases/gcl-variable-env"], {
80+
env: {
81+
...process.env,
82+
GCL_VARIABLE_MY_VAR: "hello",
83+
GCL_VARIABLE_ANOTHER_VAR: "world",
84+
},
10585
});
106-
});
86+
87+
expect(stdout).toContain("hello");
88+
expect(stdout).toContain("world");
89+
}, 30_000);

0 commit comments

Comments
 (0)