Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import stylistic from "@stylistic/eslint-plugin";

export default tseslint.config(
export default [
eslint.configs.recommended,
tseslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: [
"**/*.js",
"**/*.cjs",
".gitlab-ci-local",
"**/.gitlab-ci-local/**",
],
},
{
Expand Down Expand Up @@ -50,4 +50,4 @@ export default tseslint.config(
"key-spacing": "error",
},
},
);
];
4 changes: 2 additions & 2 deletions src/argv.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "assert";
import assert from "node:assert";
import fs from "fs-extra";
import * as dotenv from "dotenv";
import * as path from "path";
import * as path from "node:path";
import camelCase from "camelcase";
import {Utils} from "./utils.js";
import {WriteStreams} from "./write-streams.js";
Expand Down
2 changes: 1 addition & 1 deletion src/commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {WriteStreams} from "./write-streams.js";
import {Executor} from "./executor.js";
import fs from "fs-extra";
import {Argv} from "./argv.js";
import {AssertionError} from "assert";
import {AssertionError} from "node:assert";

export class Commander {

Expand Down
4 changes: 2 additions & 2 deletions src/data-expander.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from "chalk-template";
import deepExtend from "deep-extend";
import assert, {AssertionError} from "assert";
import assert, {AssertionError} from "node:assert";
import {Job, Need} from "./job.js";
import {traverse} from "object-traversal";
import {Utils} from "./utils.js";
Expand Down Expand Up @@ -150,7 +150,7 @@ export function cacheEach (jobName: string, gitlabData: any) {

jobData.cache = Array.isArray(cache) ? cache : [cache];
for (const [i, c] of Object.entries<any>(jobData.cache)) {
if (c.key?.files instanceof Array) {
if (Array.isArray(c.key?.files)) {
assert(c.key.files.length === 1 || c.key.files.length === 2, `cache:key:files should be an array of one or two file paths. Got ${c.key.files.length}`);
}
jobData.cache[i] = cacheComplex(c);
Expand Down
2 changes: 1 addition & 1 deletion src/executor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from "chalk-template";
import {Job} from "./job.js";
import assert, {AssertionError} from "assert";
import assert, {AssertionError} from "node:assert";
import {Argv} from "./argv.js";
import pMap from "p-map";

Expand Down
14 changes: 7 additions & 7 deletions src/git-data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Utils} from "./utils.js";
import assert, {AssertionError} from "assert";
import assert, {AssertionError} from "node:assert";
import {WriteStreams} from "./write-streams.js";
import chalk from "chalk-template";

Expand Down Expand Up @@ -35,12 +35,12 @@ export class GitData {

static async init (cwd: string, writeStreams: WriteStreams): Promise<GitData> {
const gitData = new GitData();
const promises = [];
promises.push(gitData.initCommitData(cwd, writeStreams));
promises.push(gitData.initRemoteData(cwd, writeStreams));
promises.push(gitData.initUserData(cwd, writeStreams));
promises.push(gitData.initBranchData(cwd, writeStreams));
await Promise.all(promises);
await Promise.all([
gitData.initCommitData(cwd, writeStreams),
gitData.initRemoteData(cwd, writeStreams),
gitData.initUserData(cwd, writeStreams),
gitData.initBranchData(cwd, writeStreams),
]);
return gitData;
}

Expand Down
4 changes: 2 additions & 2 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as yaml from "js-yaml";
import chalk from "chalk-template";
import path from "path";
import path from "node:path";
import fs from "fs-extra";
import yargs from "yargs";
import {Commander} from "./commander.js";
Expand All @@ -11,7 +11,7 @@ import {WriteStreams} from "./write-streams.js";
import {cleanupJobResources, Job} from "./job.js";
import {Utils} from "./utils.js";
import {Argv} from "./argv.js";
import assert from "assert";
import assert from "node:assert";

const generateGitIgnore = (cwd: string, stateDir: string) => {
const gitIgnoreFilePath = `${cwd}/${stateDir}/.gitignore`;
Expand Down
51 changes: 28 additions & 23 deletions src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import split2 from "split2";
import {Utils} from "./utils.js";
import {WriteStreams} from "./write-streams.js";
import {GitData} from "./git-data.js";
import assert, {AssertionError} from "assert";
import assert, {AssertionError} from "node:assert";
import {Mutex} from "./mutex.js";
import {Argv} from "./argv.js";
import execa from "execa";
Expand Down Expand Up @@ -418,10 +418,10 @@ If you know what you're doing and would like to suppress this warning, use one o
}
break;
case "object":
if (! Array.isArray(allowFailure.exit_codes)) {
allowedExitCodes = [allowFailure.exit_codes];
} else {
if (Array.isArray(allowFailure.exit_codes)) {
allowedExitCodes = allowFailure.exit_codes;
} else {
allowedExitCodes = [allowFailure.exit_codes];
}
break;
default:
Expand Down Expand Up @@ -685,12 +685,11 @@ If you know what you're doing and would like to suppress this warning, use one o
const tmpVolumeName = this.tmpVolumeName;
const fileVariablesDir = this.fileVariablesDir;

const volumePromises = [];
volumePromises.push(Utils.spawn([this.argv.containerExecutable, "volume", "create", `${buildVolumeName}`], argv.cwd));
volumePromises.push(Utils.spawn([this.argv.containerExecutable, "volume", "create", `${tmpVolumeName}`], argv.cwd));
this._containerVolumeNames.push(buildVolumeName);
this._containerVolumeNames.push(tmpVolumeName);
await Promise.all(volumePromises);
this._containerVolumeNames.push(buildVolumeName, tmpVolumeName);
await Promise.all([
Utils.spawn([this.argv.containerExecutable, "volume", "create", `${buildVolumeName}`], argv.cwd),
Utils.spawn([this.argv.containerExecutable, "volume", "create", `${tmpVolumeName}`], argv.cwd),
]);

const time = process.hrtime();
this.refreshLongRunningSilentTimeout(writeStreams);
Expand Down Expand Up @@ -762,7 +761,7 @@ If you know what you're doing and would like to suppress this warning, use one o
}

await this.execPreScripts(expanded);
if (this._prescriptsExitCode == null) throw Error("this._prescriptsExitCode must be defined!");
if (this._prescriptsExitCode == null) throw new Error("this._prescriptsExitCode must be defined!");

await this.execAfterScripts(expanded);

Expand Down Expand Up @@ -844,7 +843,7 @@ If you know what you're doing and would like to suppress this warning, use one o
scripts.forEach((script) => {
const split = script.split(/\r?\n/);
const multilineText = split.length > 1 ? " # collapsed multi-line command" : "";
const text = split[0]?.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[$]/g, "\\$");
const text = split[0]?.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`).replaceAll("$", String.raw`\$`);
if (this.interactive) {
cmd += chalk`echo "{green $ ${text}${multilineText}}"\n`;
} else {
Expand Down Expand Up @@ -1035,7 +1034,8 @@ If you know what you're doing and would like to suppress this warning, use one o

for (const [key, val] of Object.entries(expanded)) {
// Replacing `'` with `'\''` to correctly handle single quotes(if `val` contains `'`) in shell commands
dockerCmd += ` -e '${key}=${val.toString().replaceAll("'", String.raw`'\''`)}' \\\n`;
const escaped = val.toString().replaceAll("'", String.raw`'\''`);
dockerCmd += ` -e '${key}=${escaped}' \\\n`;
}

if (this.imageEntrypoint) {
Expand Down Expand Up @@ -1594,7 +1594,8 @@ If you know what you're doing and would like to suppress this warning, use one o

for (const [key, val] of Object.entries(expanded)) {
// Replacing `'` with `'\''` to correctly handle single quotes(if `val` contains `'`) in shell commands
dockerCmd += ` -e '${key}=${val.toString().replaceAll("'", String.raw`'\''`)}' \\\n`;
const escaped = val.toString().replaceAll("'", String.raw`'\''`);
dockerCmd += ` -e '${key}=${escaped}' \\\n`;
}

const serviceEntrypoint = service.entrypoint;
Expand All @@ -1621,7 +1622,10 @@ If you know what you're doing and would like to suppress this warning, use one o
dockerCmd += `${Utils.safeBashString(e)} `;
});
}
(service.command ?? []).forEach((e) => dockerCmd += `"${e.replace(/\$/g, "\\$")}" `);
for (const e of service.command ?? []) {
const escaped = e.replaceAll("$", String.raw`\$`);
dockerCmd += `"${escaped}" `;
}

const time = process.hrtime();

Expand Down Expand Up @@ -1656,14 +1660,15 @@ If you know what you're doing and would like to suppress this warning, use one o
const time = process.hrtime();
try {
// Iterate over each port defined in the image, and try to connect to the alias
await Promise.any(Object.keys(imageInspect[0].Config.ExposedPorts).map((port) => {
if (!port.endsWith("/tcp")) return;
const portNum = parseInt(port.replace("/tcp", ""));
const containerName = `gcl-wait-for-it-${this.jobId}-${serviceIndex}-${portNum}`;
const spawnCmd = [this.argv.containerExecutable, "run", "--rm", `--name=${containerName}`, "--network", `${this._serviceNetworkId}`, `${waitImageName}`, `${serviceAlias}:${portNum}`, "-t", `${waitForServicesTimeout}`];
this._containersToClean.push(containerName);
return Utils.spawn(spawnCmd);
}));
await Promise.any(Object.keys(imageInspect[0].Config.ExposedPorts)
.filter((port) => port.endsWith("/tcp"))
.map((port) => {
const portNum = Number.parseInt(port.replace("/tcp", ""));
const containerName = `gcl-wait-for-it-${this.jobId}-${serviceIndex}-${portNum}`;
const spawnCmd = [this.argv.containerExecutable, "run", "--rm", `--name=${containerName}`, "--network", `${this._serviceNetworkId}`, `${waitImageName}`, `${serviceAlias}:${portNum}`, "-t", `${waitForServicesTimeout}`];
this._containersToClean.push(containerName);
return Utils.spawn(spawnCmd);
}));
const endTime = process.hrtime(time);
writeStreams.stdout(chalk`${this.formattedJobName} {greenBright service image: ${serviceName} healthcheck passed in {green ${prettyHrtime(endTime)}}}\n`);
} catch (e: any) {
Expand Down
2 changes: 1 addition & 1 deletion src/parallel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from "assert";
import assert from "node:assert";
import deepExtend from "deep-extend";

export function isPlainParallel (jobData: any) {
Expand Down
8 changes: 4 additions & 4 deletions src/parser-includes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {Utils} from "./utils.js";
import fs from "fs-extra";
import {WriteStreams} from "./write-streams.js";
import {GitData} from "./git-data.js";
import assert, {AssertionError} from "assert";
import assert, {AssertionError} from "node:assert";
import chalk from "chalk-template";
import {Parser} from "./parser.js";
import axios from "axios";
import path from "path";
import path from "node:path";
import prettyHrtime from "pretty-hrtime";
import semver from "semver";
import {RE2JS} from "re2js";
Expand Down Expand Up @@ -433,11 +433,11 @@ export async function resolveIncludeLocal (pattern: string, cwd: string) {

// `**` matches anything
const anything = ".*?";
pattern = pattern.replaceAll(/\\\*\\\*/g, anything);
pattern = pattern.replaceAll(String.raw`\*\*`, anything);

// `*` matches anything except for `/`
const anything_but_not_slash = "([^/])*?";
pattern = pattern.replaceAll(/\\\*/g, anything_but_not_slash);
pattern = pattern.replaceAll(String.raw`\*`, anything_but_not_slash);

const re2js = RE2JS.compile(`^${pattern}`);
return repoFiles.filter((f: any) => re2js.matches(f));
Expand Down
8 changes: 4 additions & 4 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import chalk from "chalk-template";
import path from "path";
import path from "node:path";
import deepExtend from "deep-extend";
import fs from "fs-extra";
import * as yaml from "js-yaml";
import prettyHrtime from "pretty-hrtime";
import {Job} from "./job.js";
import * as DataExpander from "./data-expander.js";
import {Utils} from "./utils.js";
import assert from "assert";
import assert from "node:assert";
import {Validator} from "./validator.js";
import * as parallel from "./parallel.js";
import {GitData} from "./git-data.js";
Expand Down Expand Up @@ -199,7 +199,7 @@ export class Parser {
if (this.argv.maxJobNamePadding !== null && this.argv.maxJobNamePadding <= 0) {
this._jobNamePad = 0;
} else {
const jobs = this.argv.job.length !== 0 ? this.argv.job : this.jobs;
const jobs = this.argv.job.length === 0 ? this.jobs : this.argv.job;
jobs.forEach((job) => {
let jobNeedsLength: number[] = [];

Expand Down Expand Up @@ -304,7 +304,7 @@ export class Parser {
const uninterpolatedConfigurations: any = fileData[1];

const interpolatedConfigurations = JSON.stringify(uninterpolatedConfigurations)
.replace(
.replaceAll(
/(?<firstChar>.)?(?<secondChar>.)?\$\[\[\s*inputs.(?<interpolationKey>[\w-]+)\s*\|?\s*(?<interpolationFunctions>.*?)\s*\]\](?<lastChar>[^$])?/g // https://regexr.com/81c16
, (_: string, firstChar: string, secondChar: string, interpolationKey: string, interpolationFunctions: string, lastChar: string) => {
const configFilePath = path.relative(process.cwd(), filePath);
Expand Down
4 changes: 2 additions & 2 deletions src/predefined-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ export function init ({gitData, argv, envMatchedVariables}: PredefinedVariablesO
CI_PROJECT_NAME: gitData.remote.project,
CI_PROJECT_TITLE: `${camelCase(gitData.remote.project)}`,
CI_PROJECT_PATH: `${gitData.remote.group}/${gitData.remote.project}`,
CI_PROJECT_PATH_SLUG: `${gitData.remote.group.replace(/\//g, "-")}-${gitData.remote.project}`.toLowerCase(),
CI_PROJECT_PATH_SLUG: `${gitData.remote.group.replaceAll("/", "-")}-${gitData.remote.project}`.toLowerCase(),
CI_PROJECT_ROOT_NAMESPACE: CI_PROJECT_ROOT_NAMESPACE,
CI_PROJECT_NAMESPACE: CI_PROJECT_NAMESPACE,
CI_PROJECT_VISIBILITY: "internal",
CI_PROJECT_ID: "1217",
CI_COMMIT_REF_PROTECTED: "false",
CI_COMMIT_BRANCH: gitData.commit.REF_NAME, // Not available in merge request or tag pipelines
CI_COMMIT_REF_NAME: gitData.commit.REF_NAME, // Tag or branch name
CI_COMMIT_REF_SLUG: gitData.commit.REF_NAME.replace(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 63).replace(/-$/, "").toLowerCase(),
CI_COMMIT_REF_SLUG: gitData.commit.REF_NAME.replaceAll(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 63).replace(/-$/, "").toLowerCase(),
CI_COMMIT_TIMESTAMP: gitData.commit.TIMESTAMP,
CI_PIPELINE_CREATED_AT: new Date().toISOString().split(".")[0] + "Z",
CI_COMMIT_TITLE: "Commit Title", // First line of commit message.
Expand Down
15 changes: 7 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import fs from "fs-extra";
import checksum from "checksum";
import base64url from "base64url";
import execa, {ExecaError} from "execa";
import assert from "assert";
import assert from "node:assert";
import {CICDVariable} from "./variables-from-files.js";
import {GitData} from "./git-data.js";
import {globbySync} from "globby";
Expand Down Expand Up @@ -92,7 +92,7 @@ export class Utils {
const matches = Array.from(content.matchAllRE2JS(regex));
if (matches.length === 0) return "0";

const lastMatch = matches[matches.length - 1];
const lastMatch = matches.at(-1)!;
const digits = /\d+(?:\.\d+)?/.exec(lastMatch[1] ?? lastMatch[0] ?? "");
if (!digits) return "0";
return digits[0] ?? "0";
Expand All @@ -111,16 +111,15 @@ export class Utils {
return text;
}

return text.replace(
return text.replaceAll(
/(\$\$)|\$\{([a-zA-Z_]\w*)}|\$([a-zA-Z_]\w*)/g, // https://regexr.com/7s4ka
(_match, escape, var1, var2) => {
if (typeof escape !== "undefined") {
if (escape !== undefined) {
return expandWith.unescape;
} else {
const name = var1 || var2;
assert(name, "unexpected unset capture group");
return `${expandWith.variable(name)}`;
}
const name = var1 || var2;
assert(name, "unexpected unset capture group");
return `${expandWith.variable(name)}`;
},
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ajv from "ajv";
import {Job} from "./job.js";
import assert from "assert";
import assert from "node:assert";
import chalk from "chalk-template";
import schema from "./schema.js";
import {betterAjvErrors} from "./schema-error.js";
Expand Down
8 changes: 4 additions & 4 deletions src/variables-from-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as yaml from "js-yaml";
import chalk from "chalk-template";
import prettyHrtime from "pretty-hrtime";
import {Argv} from "./argv.js";
import assert from "assert";
import assert from "node:assert";
import {Utils} from "./utils.js";
import dotenv from "dotenv";
import deepExtend from "deep-extend";
Expand Down Expand Up @@ -72,20 +72,20 @@ export class VariablesFromFiles {
for (const [matcher, content] of Object.entries(values)) {
assert(typeof content == "string", `${key}.${matcher} content must be text or multiline text`);
if (isDotEnv || type === "variable" || (type === null && !/^[/~]/.exec(content))) {
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replace(/\*/g, ".*")}$`, "g");
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replaceAll("*", ".*")}$`, "g");

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Escape regex metacharacters in matcher before converting * to .*; current construction can mis-match scopes or throw on invalid patterns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/variables-from-files.ts, line 75:

<comment>Escape regex metacharacters in `matcher` before converting `*` to `.*`; current construction can mis-match scopes or throw on invalid patterns.</comment>

<file context>
@@ -72,20 +72,20 @@ export class VariablesFromFiles {
                 assert(typeof content == "string", `${key}.${matcher} content must be text or multiline text`);
                 if (isDotEnv || type === "variable" || (type === null && !/^[/~]/.exec(content))) {
-                    const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replace(/\*/g, ".*")}$`, "g");
+                    const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replaceAll("*", ".*")}$`, "g");
                     variables[key] = variables[key] ?? {type: "variable", environments: []};
                     variables[key].environments.push({content, regexp, regexpPriority: matcher.length, scopePriority});
</file context>
Fix with Cubic

variables[key] = variables[key] ?? {type: "variable", environments: []};
variables[key].environments.push({content, regexp, regexpPriority: matcher.length, scopePriority});
} else if (type === null && /^[/~]/.exec(content)) {
const fileSource = content.replace(/^~\/(.*)/, `${homeDir}/$1`);
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replace(/\*/g, ".*")}$`, "g");
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replaceAll("*", ".*")}$`, "g");
variables[key] = variables[key] ?? {type: "file", environments: []};
if (fs.existsSync(fileSource)) {
variables[key].environments.push({content, regexp, regexpPriority: matcher.length, scopePriority, fileSource});
} else {
variables[key].environments.push({content: `warn: ${key} is pointing to invalid path\n`, regexp, regexpPriority: matcher.length, scopePriority});
}
} else if (type === "file") {
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replace(/\*/g, ".*")}$`, "g");
const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replaceAll("*", ".*")}$`, "g");
variables[key] = variables[key] ?? {type: "file", environments: []};
variables[key].environments.push({content, regexp, regexpPriority: matcher.length, scopePriority});
} else {
Expand Down
Loading