Skip to content
14 changes: 14 additions & 0 deletions src/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@
return stdout;
}

export function injectGclVariableEnvVars (argv: {variable?: string[]}, env: Record<string, string | undefined>): void {
const prefix = "GCL_VARIABLE_";
for (const [envKey, envValue] of Object.entries(env)) {
if (!envKey.startsWith(prefix) || envValue == null) continue;
const varName = envKey.slice(prefix.length);
if (varName.length === 0) continue;
if (argv.variable == null) {
argv.variable = [];
}

Check warning on line 33 in src/argv.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using nullish coalescing operator (`??=`) instead of an assignment expression, as it is simpler to read.

See more on https://sonarcloud.io/project/issues?id=firecow_gitlab-ci-local&issues=AZ0FSSLVnt4r1Upcc1Ei&open=AZ0FSSLVnt4r1Upcc1Ei&pullRequest=1805
argv.variable.unshift(`${varName}=${envValue}`);
}
}

export class Argv {
static readonly default = {
"variablesFile": ".gitlab-ci-local-variables.yml",
Expand All @@ -43,6 +56,7 @@
}

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

Expand Down
6 changes: 6 additions & 0 deletions tests/test-cases/gcl-variable-env/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
test-job:
script:
- echo ${MY_VAR}
- echo ${ANOTHER_VAR}
- echo ${EMPTY_VAR}
98 changes: 98 additions & 0 deletions tests/test-cases/gcl-variable-env/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {WriteStreamsMock} from "../../../src/write-streams.js";
import {handler} from "../../../src/handler.js";
import {injectGclVariableEnvVars} from "../../../src/argv.js";
import chalk from "chalk-template";
import {initSpawnSpy} from "../../mocks/utils.mock.js";
import {WhenStatics} from "../../mocks/when-statics.js";

beforeAll(() => {
initSpawnSpy(WhenStatics.all);
});

describe("injectGclVariableEnvVars unit tests", () => {
test("injects single GCL_VARIABLE_ entry", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_MY_VAR": "hello"});
expect(argv.variable).toEqual(["MY_VAR=hello"]);
});

test("injects multiple GCL_VARIABLE_ entries", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {
"GCL_VARIABLE_VAR1": "one",
"GCL_VARIABLE_VAR2": "two",
});
expect(argv.variable).toEqual(expect.arrayContaining(["VAR1=one", "VAR2=two"]));
expect(argv.variable).toHaveLength(2);
});

test("prepends to existing variable array", () => {
const argv: {variable?: string[]} = {variable: ["EXISTING=val"]};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_NEW": "injected"});
expect(argv.variable).toEqual(["NEW=injected", "EXISTING=val"]);
});

test("CLI --variable takes precedence over GCL_VARIABLE_", () => {
const argv: {variable?: string[]} = {variable: ["SAME=from_cli"]};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_SAME": "from_env"});
// "SAME=from_env" is prepended, "SAME=from_cli" comes last and wins
expect(argv.variable).toEqual(["SAME=from_env", "SAME=from_cli"]);
});

test("skips non GCL_VARIABLE_ env vars", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {
"GCL_CWD": "/tmp",
"HOME": "/home/user",
"GCL_VARIABLE_REAL": "yes",
});
expect(argv.variable).toEqual(["REAL=yes"]);
});

test("skips GCL_VARIABLE_ with empty name", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_": "empty_name"});
expect(argv.variable).toBeUndefined();
});

test("skips null/undefined env values", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_FOO": undefined});
expect(argv.variable).toBeUndefined();
});

test("handles empty value", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_FOO": ""});
expect(argv.variable).toEqual(["FOO="]);
});

test("handles value with equals sign", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_FOO": "key=value"});
expect(argv.variable).toEqual(["FOO=key=value"]);
});

test("does not split on semicolons", () => {
const argv: {variable?: string[]} = {};
injectGclVariableEnvVars(argv, {"GCL_VARIABLE_FOO": "a;b;c"});
expect(argv.variable).toEqual(["FOO=a;b;c"]);
});
});

describe("injectGclVariableEnvVars integration", () => {
test.concurrent("GCL_VARIABLE_MY_VAR=hello injects variable into job", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/gcl-variable-env",
job: ["test-job"],
variable: ["MY_VAR=hello", "ANOTHER_VAR=world", "EMPTY_VAR="],
}, writeStreams);

const expected = [
chalk`{blueBright test-job} {greenBright >} hello`,
chalk`{blueBright test-job} {greenBright >} world`,
];
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});
});
Loading