diff --git a/README.md b/README.md index 3240f5b1..dedfb0d5 100644 --- a/README.md +++ b/README.md @@ -274,17 +274,32 @@ gitlab-ci-local --remote-variables git@gitlab.com:firecow/example.git=gitlab-var ### Project file variables -Put a file like this in `$CWD/.gitlab-ci-local-variables.yml` +The `--variables-file` [default: $CWD/.gitlab-ci-local-variables.yml] can be used to setup the CI/CD variables for the executors +#### `yaml` format ```yaml --- AUTHORIZATION_PASSWORD: djwqiod910321 DOCKER_LOGIN_PASSWORD: dij3213n123n12in3 # Will be type File, because value is a file path KNOWN_HOSTS: '~/.ssh/known_hosts' + +# This is only supported in the yaml format +# https://docs.gitlab.com/ee/ci/environments/index.html#limit-the-environment-scope-of-a-cicd-variable +EXAMPLE: + values: + "*": "I am only available in all jobs" + staging: "I am only available in jobs with `environment: staging`" + production: "I am only available in jobs with `environment: production`" ``` -Variables will now appear in your jobs. +#### `.env` format +``` +AUTHORIZATION_PASSWORD=djwqiod910321 +DOCKER_LOGIN_PASSWORD=dij3213n123n12in3 +# NOTE: value will be '~/.ssh/known_hosts' which is different behavior from the yaml format +KNOWN_HOSTS='~/.ssh/known_hosts' +``` ### Decorators diff --git a/src/variables-from-files.ts b/src/variables-from-files.ts index fb853560..a907f7b9 100644 --- a/src/variables-from-files.ts +++ b/src/variables-from-files.ts @@ -6,6 +6,7 @@ import chalk from "chalk"; import {Argv} from "./argv.js"; import assert from "assert"; import {Utils} from "./utils.js"; +import dotenv from "dotenv"; export interface CICDVariable { type: "file" | "variable"; @@ -56,11 +57,11 @@ export class VariablesFromFiles { } return v; }; - const addToVariables = async (key: string, val: any, scopePriority: number) => { + const addToVariables = async (key: string, val: any, scopePriority: number, isDotEnv = false) => { const {type, values} = unpack(val); for (const [matcher, content] of Object.entries(values)) { assert(typeof content == "string", `${key}.${matcher} content must be text or multiline text`); - if (type === "variable" || (type === null && !/^[/|~]/.exec(content))) { + if (isDotEnv || type === "variable" || (type === null && !/^[/|~]/.exec(content))) { const regexp = matcher === "*" ? /.*/g : new RegExp(`^${matcher.replace(/\*/g, ".*")}$`, "g"); variables[key] = variables[key] ?? {type: "variable", environments: []}; variables[key].environments.push({content, regexp, regexpPriority: matcher.length, scopePriority}); @@ -114,11 +115,26 @@ export class VariablesFromFiles { const projectVariablesFile = `${argv.cwd}/${argv.variablesFile}`; if (fs.existsSync(projectVariablesFile)) { - const projectVariablesFileData: any = yaml.load(await fs.readFile(projectVariablesFile, "utf8"), {schema: yaml.FAILSAFE_SCHEMA}) ?? {}; + let isDotEnvFormat = false; + const projectVariablesFileRawContent = await fs.readFile(projectVariablesFile, "utf8"); + let projectVariablesFileData; + try { + projectVariablesFileData = yaml.load(projectVariablesFileRawContent, {schema: yaml.FAILSAFE_SCHEMA}) ?? {}; + + if (typeof(projectVariablesFileData) === "string") { + isDotEnvFormat = true; + projectVariablesFileData = dotenv.parse(projectVariablesFileRawContent); + } + } catch (e) { + if (e instanceof yaml.YAMLException) { + isDotEnvFormat = true; + projectVariablesFileData = dotenv.parse(projectVariablesFileRawContent); + } + } assert(projectVariablesFileData != null, "projectEntries cannot be null/undefined"); assert(Utils.isObject(projectVariablesFileData), `${argv.cwd}/.gitlab-ci-local-variables.yml must contain an object`); for (const [k, v] of Object.entries(projectVariablesFileData)) { - await addToVariables(k, v, 24); + await addToVariables(k, v, 24, isDotEnvFormat); } } diff --git a/tests/test-cases/project-variables-file/.env b/tests/test-cases/project-variables-file/.env new file mode 100644 index 00000000..6879496e --- /dev/null +++ b/tests/test-cases/project-variables-file/.env @@ -0,0 +1 @@ +SECRET="holycow" diff --git a/tests/test-cases/project-variables-file/.envs b/tests/test-cases/project-variables-file/.envs new file mode 100644 index 00000000..8bfd6f81 --- /dev/null +++ b/tests/test-cases/project-variables-file/.envs @@ -0,0 +1,23 @@ +SECRET_APP_DEBUG=true +SECRET_APP_ENV=local +SECRET_APP_KEY= +SECRET_APP_NAME="Laravel" +SECRET_APP_URL=http://localhost +SECRET_BROADCAST_DRIVER=log +SECRET_CACHE_DRIVER=file +SECRET_DB_CONNECTION=mysql +SECRET_DB_DATABASE=laravel +SECRET_DB_HOST=127.0.0.1 +SECRET_DB_PASSWORD= +SECRET_DB_PORT=3306 +# comments are allowed in .env +SECRET_DB_USERNAME=root +SECRET_FILESYSTEM_DISK=local +SECRET_KNOWN_HOSTS="~/known_hosts" +SECRET_LOG_CHANNEL=stack +SECRET_LOG_DEPRECATIONS_CHANNEL=null +SECRET_LOG_LEVEL=debug +SECRET_MEMCACHED_HOST=127.0.0.1 +SECRET_QUEUE_CONNECTION=sync +SECRET_SESSION_DRIVER=file +SECRET_SESSION_LIFETIME=120 diff --git a/tests/test-cases/project-variables-file/.gitlab-ci-custom.yml b/tests/test-cases/project-variables-file/.gitlab-ci-custom.yml index be2cad7c..4bec5e1f 100644 --- a/tests/test-cases/project-variables-file/.gitlab-ci-custom.yml +++ b/tests/test-cases/project-variables-file/.gitlab-ci-custom.yml @@ -3,3 +3,8 @@ job: image: busybox script: - echo $SECRET + +job2: + image: busybox + script: + - env | grep SECRET | sort diff --git a/tests/test-cases/project-variables-file/integration.project-variables-file.test.ts b/tests/test-cases/project-variables-file/integration.project-variables-file.test.ts index 3e7fc592..126d5c0c 100644 --- a/tests/test-cases/project-variables-file/integration.project-variables-file.test.ts +++ b/tests/test-cases/project-variables-file/integration.project-variables-file.test.ts @@ -3,15 +3,25 @@ import {handler} from "../../../src/handler.js"; import chalk from "chalk"; import {initSpawnSpy} from "../../mocks/utils.mock.js"; import {WhenStatics} from "../../mocks/when-statics.js"; +import fs from "fs-extra"; +import path from "path"; +import {stripAnsi} from "../../utils"; +const cwd = "tests/test-cases/project-variables-file"; +const emptyFileVariable = "dummy"; beforeAll(() => { initSpawnSpy([...WhenStatics.all]); + fs.createFileSync(path.join(cwd, emptyFileVariable)); +}); + +afterAll(() => { + fs.removeSync(path.join(cwd, emptyFileVariable)); }); test.concurrent("project-variables-file ", async () => { const writeStreams = new WriteStreamsMock(); await handler({ - cwd: "tests/test-cases/project-variables-file", + cwd: cwd, job: ["test-job"], }, writeStreams); @@ -25,7 +35,7 @@ test.concurrent("project-variables-file ", async () => { test.concurrent("project-variables-file ", async () => { const writeStreams = new WriteStreamsMock(); await handler({ - cwd: "tests/test-cases/project-variables-file", + cwd: cwd, file: ".gitlab-ci-issue-1333.yml", }, writeStreams); @@ -38,9 +48,10 @@ test.concurrent("project-variables-file ", async () => { test.concurrent("project-variables-file custom-path", async () => { const writeStreams = new WriteStreamsMock(); await handler({ - cwd: "tests/test-cases/project-variables-file", + cwd: cwd, file: ".gitlab-ci-custom.yml", variablesFile: ".custom-local-var-file", + job: ["job"], }, writeStreams); const expected = [ @@ -48,3 +59,83 @@ test.concurrent("project-variables-file custom-path", async () => { ]; expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected)); }); + +test.concurrent("project-variables-file empty-variable-file", async () => { + const writeStreams = new WriteStreamsMock(); + await handler({ + cwd: cwd, + file: ".gitlab-ci-custom.yml", + variablesFile: emptyFileVariable, + job: ["job"], + preview: true, + }, writeStreams); + expect(writeStreams.stdoutLines[0]).toEqual(`--- +stages: + - .pre + - build + - test + - deploy + - .post +job: + image: + name: busybox + script: + - echo $SECRET +job2: + image: + name: busybox + script: + - env | grep SECRET | sort`); +}); + +test.concurrent("project-variables-file custom-path (.env)", async () => { + const writeStreams = new WriteStreamsMock(); + await handler({ + cwd: cwd, + file: ".gitlab-ci-custom.yml", + variablesFile: ".env", + job: ["job"], + }, writeStreams); + + const expected = [ + chalk`{blueBright job} {greenBright >} holycow`, + ]; + expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected)); +}); + +test.concurrent("project-variables-file custom-path (.envs)", async () => { + const writeStreams = new WriteStreamsMock(); + await handler({ + cwd: cwd, + file: ".gitlab-ci-custom.yml", + job: ["job2"], + variablesFile: ".envs", + }, writeStreams); + + const expected = ` +job2 > SECRET_APP_DEBUG=true +job2 > SECRET_APP_ENV=local +job2 > SECRET_APP_KEY= +job2 > SECRET_APP_NAME=Laravel +job2 > SECRET_APP_URL=http://localhost +job2 > SECRET_BROADCAST_DRIVER=log +job2 > SECRET_CACHE_DRIVER=file +job2 > SECRET_DB_CONNECTION=mysql +job2 > SECRET_DB_DATABASE=laravel +job2 > SECRET_DB_HOST=127.0.0.1 +job2 > SECRET_DB_PASSWORD= +job2 > SECRET_DB_PORT=3306 +job2 > SECRET_DB_USERNAME=root +job2 > SECRET_FILESYSTEM_DISK=local +job2 > SECRET_KNOWN_HOSTS=~/known_hosts +job2 > SECRET_LOG_CHANNEL=stack +job2 > SECRET_LOG_DEPRECATIONS_CHANNEL=null +job2 > SECRET_LOG_LEVEL=debug +job2 > SECRET_MEMCACHED_HOST=127.0.0.1 +job2 > SECRET_QUEUE_CONNECTION=sync +job2 > SECRET_SESSION_DRIVER=file +job2 > SECRET_SESSION_LIFETIME=120`; + + const stdout = stripAnsi(writeStreams.stdoutLines.join("\n")); + expect(stdout).toContain(expected); +});