Skip to content

Commit e230024

Browse files
firecowmjncegodk
andauthored
Fix GCL_ array env vars not splitting semicolon-separated values (#1778)
Co-authored-by: Mads Jon Nielsen <mjn@cego.dk>
1 parent 34069b3 commit e230024

5 files changed

Lines changed: 334 additions & 56 deletions

File tree

src/argv.ts

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import {WriteStreams} from "./write-streams.js";
88
import chalkBase from "chalk";
99
import chalk from "chalk-template";
1010

11+
export function splitSemicolonEnvVars (argv: Record<string, any>, arrayKeys: Set<string>, env: Record<string, string | undefined>): void {
12+
for (const [envKey, envValue] of Object.entries(env)) {
13+
if (!envKey.startsWith("GCL_") || envValue == null) continue;
14+
const optionName = camelCase(envKey.slice(4));
15+
if (!arrayKeys.has(optionName)) continue;
16+
const currentVal = argv[optionName];
17+
if (!Array.isArray(currentVal) || currentVal.length !== 1 || currentVal[0] !== envValue) continue;
18+
argv[optionName] = envValue.split(";");
19+
}
20+
}
21+
1122
async function isInGitRepository () {
1223
try {
1324
await Utils.spawn(["git", "rev-parse", "--is-inside-work-tree"]);
@@ -103,36 +114,43 @@ export class Argv {
103114
}
104115

105116
private injectDotenv (potentialDotenvFilepath: string, argv: any) {
106-
if (fs.existsSync(potentialDotenvFilepath)) {
107-
const config = dotenv.parse(fs.readFileSync(potentialDotenvFilepath));
108-
for (const [key, value] of Object.entries(config)) {
109-
const argKey = camelCase(key);
110-
111-
// Special handle KEY=VALUE variable keys
112-
if (argKey === "variable") {
113-
let currentVal = argv[argKey];
114-
if (currentVal == null) {
115-
currentVal = [];
116-
this.map.set(argKey, currentVal);
117-
}
118-
if (!Array.isArray(currentVal)) {
119-
continue;
120-
}
121-
for (const pair of value.split(" ")) {
122-
currentVal.unshift(pair);
123-
}
124-
} else if (argv[argKey] == null) {
125-
// Work around `dotenv.parse` limitation https://github.com/motdotla/dotenv/issues/51#issuecomment-552559070
126-
if (value === "true") this.map.set(argKey, true);
127-
else if (value === "false") this.map.set(argKey, false);
128-
else if (value === "null") this.map.set(argKey, null);
129-
else if (!isNaN(Number(value))) this.map.set(argKey, Number(value));
130-
else this.map.set(argKey, value);
117+
if (!fs.existsSync(potentialDotenvFilepath)) return;
118+
119+
const config = dotenv.parse(fs.readFileSync(potentialDotenvFilepath));
120+
for (const [key, value] of Object.entries(config)) {
121+
const argKey = camelCase(key);
122+
123+
// variable is additive — merge dotenv values with CLI values
124+
if (argKey === "variable") {
125+
let currentVal = argv[argKey];
126+
if (currentVal == null) {
127+
currentVal = [];
128+
this.map.set(argKey, currentVal);
129+
}
130+
if (!Array.isArray(currentVal)) {
131+
continue;
131132
}
133+
for (const pair of value.split(" ")) {
134+
currentVal.unshift(pair);
135+
}
136+
} else if (argv[argKey] == null) {
137+
// Work around `dotenv.parse` limitation https://github.com/motdotla/dotenv/issues/51#issuecomment-552559070
138+
if (value === "true") this.map.set(argKey, true);
139+
else if (value === "false") this.map.set(argKey, false);
140+
else if (value === "null") this.map.set(argKey, null);
141+
else if (!isNaN(Number(value))) this.map.set(argKey, Number(value));
142+
else this.map.set(argKey, value);
132143
}
133144
}
134145
}
135146

147+
private getStringArray (key: string): string[] {
148+
const val = this.map.get(key) ?? [];
149+
if (Array.isArray(val)) return val;
150+
if (typeof val === "string") return val.split(" ");
151+
return [];
152+
}
153+
136154
get cwd (): string {
137155
let cwd = this.map.get("cwd") ?? ".";
138156
assert(typeof cwd != "object", "--cwd option cannot be an array");
@@ -163,20 +181,11 @@ export class Argv {
163181
return (this.map.get("home") ?? process.env.HOME ?? "").replace(/\/$/, "");
164182
}
165183

166-
get volume (): string[] {
167-
const val = this.map.get("volume") ?? [];
168-
return typeof val == "string" ? val.split(" ") : val;
169-
}
184+
get volume (): string[] { return this.getStringArray("volume"); }
170185

171-
get network (): string[] {
172-
const val = this.map.get("network") ?? [];
173-
return typeof val == "string" ? val.split(" ") : val;
174-
}
186+
get network (): string[] { return this.getStringArray("network"); }
175187

176-
get extraHost (): string[] {
177-
const val = this.map.get("extraHost") ?? [];
178-
return typeof val == "string" ? val.split(" ") : val;
179-
}
188+
get extraHost (): string[] { return this.getStringArray("extraHost"); }
180189

181190
get caFile (): string | null {
182191
return this.map.get("caFile") ?? null;
@@ -194,32 +203,26 @@ export class Argv {
194203
return this.map.get("pullPolicy") ?? "if-not-present";
195204
}
196205

197-
get remoteVariables (): string[] {
198-
const val = this.map.get("remoteVariables") ?? [];
199-
return typeof val == "string" ? val.split(" ") : val;
200-
}
206+
get remoteVariables (): string[] { return this.getStringArray("remoteVariables"); }
201207

202208
get variable (): {[key: string]: string} {
203-
const val = this.map.get("variable");
204209
const variables: {[key: string]: string} = {};
205-
const pairs = typeof val == "string" ? val.split(" ") : val;
206-
(pairs ?? []).forEach((variablePair: string) => {
207-
const exec = /(?<key>\w*?)(=)(?<value>(.|\n|\r)*)/.exec(variablePair);
208-
if (exec?.groups?.key) {
209-
variables[exec.groups.key] = exec?.groups?.value;
210+
for (const pair of this.getStringArray("variable")) {
211+
const eqIndex = pair.indexOf("=");
212+
if (eqIndex < 1) continue;
213+
const key = pair.substring(0, eqIndex);
214+
if (/^\w+$/.test(key)) {
215+
variables[key] = pair.substring(eqIndex + 1);
210216
}
211-
});
217+
}
212218
return variables;
213219
}
214220

215221
get unsetVariables (): string[] {
216222
return this.map.get("unsetVariable") ?? [];
217223
}
218224

219-
get manual (): string[] {
220-
const val = this.map.get("manual") ?? [];
221-
return typeof val == "string" ? val.split(" ") : val;
222-
}
225+
get manual (): string[] { return this.getStringArray("manual"); }
223226

224227
get job (): string[] {
225228
return this.map.get("job") ?? [];
@@ -250,10 +253,7 @@ export class Argv {
250253
return this.map.get("privileged") ?? false;
251254
}
252255

253-
get device (): string[] {
254-
const val = this.map.get("device") ?? [];
255-
return typeof val == "string" ? val.split(" ") : val;
256-
}
256+
get device (): string[] { return this.getStringArray("device"); }
257257

258258
get ulimit (): string | null {
259259
const ulimit = this.map.get("ulimit");

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env node
22
import chalk from "chalk-template";
33
import yargs from "yargs";
4+
import camelCase from "camelcase";
5+
import {splitSemicolonEnvVars} from "./argv.js";
46
import {Parser} from "./parser.js";
57
import * as state from "./state.js";
68
import {WriteStreamsProcess, WriteStreamsMock} from "./write-streams.js";
@@ -42,6 +44,8 @@ process.on("SIGUSR2", async () => {
4244
.command({
4345
handler: async (argv) => {
4446
try {
47+
const arrayKeys = new Set(yparser.getOptions().array.map((k: string) => camelCase(k)));
48+
splitSemicolonEnvVars(argv, arrayKeys, process.env);
4549
injectGclVariableEnvVars(argv, gclVariableEnvVars);
4650
await handler(argv, new WriteStreamsProcess(), jobs);
4751
const failedJobs = Executor.getFailed(jobs);

tests/test-cases/cli-option-variables/integration.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,21 @@ line string`],
2626
];
2727
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
2828
});
29+
30+
test("cli-option-variables --variable with semicolons in value preserves them", async () => {
31+
const writeStreams = new WriteStreamsMock();
32+
await handler({
33+
cwd: "tests/test-cases/cli-option-variables",
34+
job: ["test-job"],
35+
variable: ["CLI_VAR=host=db;port=5432", "CLI_VAR_DOT=dotdot", `CLI_MULTILINE=This is a multi
36+
line string`],
37+
}, writeStreams);
38+
39+
const expected = [
40+
chalk`{blueBright test-job} {greenBright >} host=db;port=5432`,
41+
chalk`{blueBright test-job} {greenBright >} dotdot`,
42+
chalk`{blueBright test-job} {greenBright >} This is a multi`,
43+
chalk`{blueBright test-job} {greenBright >} line string`,
44+
];
45+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
46+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
test-job:
3+
script:
4+
- echo ${VAR1}
5+
- echo ${VAR2}
6+
- echo ${VAR3}

0 commit comments

Comments
 (0)