Skip to content

Commit

Permalink
feat: add support for .env format for variables-file
Browse files Browse the repository at this point in the history
  • Loading branch information
ANGkeith committed Oct 11, 2024
1 parent 3e8242d commit 2901e8e
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 9 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions src/variables-from-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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);
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/test-cases/project-variables-file/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SECRET="holycow"
23 changes: 23 additions & 0 deletions tests/test-cases/project-variables-file/.envs
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/test-cases/project-variables-file/.gitlab-ci-custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ job:
image: busybox
script:
- echo $SECRET

job2:
image: busybox
script:
- env | grep SECRET | sort
Original file line number Diff line number Diff line change
Expand Up @@ -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 <test-job>", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/project-variables-file",
cwd: cwd,
job: ["test-job"],
}, writeStreams);

Expand All @@ -25,7 +35,7 @@ test.concurrent("project-variables-file <test-job>", async () => {
test.concurrent("project-variables-file <issue-1333>", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/project-variables-file",
cwd: cwd,
file: ".gitlab-ci-issue-1333.yml",
}, writeStreams);

Expand All @@ -38,13 +48,94 @@ test.concurrent("project-variables-file <issue-1333>", 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 = [
chalk`{blueBright job} {greenBright >} firecow`,
];
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);
});

0 comments on commit 2901e8e

Please sign in to comment.