Skip to content

Commit 717d78b

Browse files
committed
feat(envs): add abstruse defined ENV variables (closes #311)
1 parent 92a393b commit 717d78b

File tree

6 files changed

+181
-24
lines changed

6 files changed

+181
-24
lines changed

docs/ENV_VARIABLES.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Environment Variables
2+
3+
A common way to customize the build process is to define environment variables, which can be accessed from any stage in your build process.
4+
5+
### Public ENV variables reference
6+
7+
- `ABSTRUSE_BRANCH`
8+
- for push builds, or builds not triggered by a pull request, this is the name of the branch.
9+
- for builds triggered by a pull request this is the name of the branch targeted by the pull request.
10+
- for builds triggered by a tag, this is the same as the name of the tag (`ABSTRUSE_TAG`)
11+
12+
- `ABSTRUSE_BUILD_DIR`
13+
- The absolute path to the directory where the repository being built has been copied on the worker.
14+
15+
- `ABSTRUSE_BUILD_ID`
16+
- The id of the current build that Abstruse CI uses internally.
17+
18+
- `ABSTRUSE_JOB_ID`
19+
- The if of the current job that Abstruse CI uses internally.
20+
21+
- `ABSTRUSE_COMMIT`
22+
- The commit that the current build is testing.
23+
24+
- `ABSTRUSE_EVENT_TYPE`
25+
- Indicates how the build was triggered. One of `push` or `pull_request`
26+
27+
- `ABSTRUSE_PULL_REQUEST`
28+
- The pull request number if the current job is a pull request, “false” if it’s not a pull request.
29+
30+
- `ABSTRUSE_PULL_REQUEST_BRANCH`
31+
- if the current job is a pull request, the name of the branch from which the PR originated.
32+
- if the current job is a push build, this variable is empty (`""`)
33+
34+
- `ABSTRUSE_TAG`
35+
- If the current build is for a git tag, this variable is set to the tag’s name.
36+
37+
- `ABSTRUSE_PULL_REQUEST_SHA`
38+
- if the current job is a pull request, the commit SHA of the HEAD commit of the PR.
39+
- if the current job is a push build, this variable is empty (`""`)
40+
41+
- `ABSTRUSE_SECURE_ENV_VARS`
42+
- Set to `true` if there are any encrypted environment variables.
43+
- Set to `false` if no encrypted environment variables are available.
44+
45+
- `ABSTRUSE_TEST_RESULT`
46+
- is set to `0` if the build is successful and `1-255` if the build is broken.
47+
- this variable is available only since `test` command is executed
48+
49+
### Define public ENV variables in .abstruse.yml
50+
51+
You can define multiple ENV variables per item.
52+
53+
```yml
54+
matrix:
55+
- env: SCRIPT=lint NODE_VERSION=8
56+
- env: SCRIPT=test NODE_VERSION=8
57+
- env: SCRIPT=test:e2e NODE_VERSION=8
58+
- env: SCRIPT=test:protractor NODE_VERSION=8
59+
- env: SCRIPT=test:karma NODE_VERSION=8
60+
```
61+
62+
### Define variables public and encrypted variables under repository
63+
64+
Variables defined in repository settings are the same for all builds, and when you restart an old build, it uses the latest values. These variables are not automatically available to forks.
65+
66+
Define variables in the Repository Settings that:
67+
68+
- differ per repository.
69+
- contain sensitive data, such as third-party credentials (encrypted variables).
70+
71+
<img src="https://user-images.githubusercontent.com/1796022/34071301-9d4e4d04-e274-11e7-8be7-57f411d3f93f.png">

src/api/db/job.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ export function getJob(jobId: number, userId?: number): Promise<any> {
1111
.andWhere('permissions.permission', true)
1212
.orWhere('public', true);
1313
}
14-
}},
15-
'runs']})
14+
}}, 'runs']})
1615
.then(job => {
1716
if (!job) {
1817
reject();

src/api/docker.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as dockerode from 'dockerode';
55
import { Writable } from 'stream';
66
import { CommandType } from './config';
77
import { ProcessOutput } from './process';
8+
import * as envVars from './env-variables';
89
import chalk from 'chalk';
910
import * as style from 'ansi-styles';
1011

@@ -59,9 +60,8 @@ export function startContainer(id: string): Promise<dockerode.Container> {
5960
return docker.getContainer(id).start();
6061
}
6162

62-
export function dockerExec(id: string, cmd: any, env: string[] = []): Observable<any> {
63+
export function dockerExec(id: string, cmd: any, env: envVars.EnvVariables = {}): Observable<any> {
6364
return new Observable(observer => {
64-
const startTime = new Date().getTime();
6565
let exitCode = 255;
6666
let command;
6767

@@ -89,7 +89,7 @@ export function dockerExec(id: string, cmd: any, env: string[] = []): Observable
8989
const container = docker.getContainer(id);
9090
const execOptions = {
9191
Cmd: ['/usr/bin/abstruse-pty', cmd.command],
92-
Env: env,
92+
Env: envVars.serialize(env),
9393
AttachStdout: true,
9494
AttachStderr: true,
9595
Tty: true
@@ -102,7 +102,11 @@ export function dockerExec(id: string, cmd: any, env: string[] = []): Observable
102102
ws.setDefaultEncoding('utf8');
103103

104104
ws.on('finish', () => {
105-
const duration = new Date().getTime() - startTime;
105+
if (cmd.type === CommandType.script) {
106+
envVars.set(env, 'ABSTRUSE_TEST_RESULT', exitCode);
107+
}
108+
109+
observer.next({ type: 'env', data: env });
106110
observer.next({ type: 'exit', data: exitCode });
107111
observer.complete();
108112
});

src/api/env-variables.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export interface EnvVariables {
2+
[key: string]: string | number | boolean;
3+
}
4+
5+
export function set(envs: EnvVariables, key: string, value: string | number | boolean): void {
6+
envs[key] = value;
7+
}
8+
9+
export function unset(envs: EnvVariables, key: string): void {
10+
envs[key] = null;
11+
}
12+
13+
export function serialize(envs: EnvVariables): string[] {
14+
return Object.keys(envs).map(key => `${key}=${envs[key]}`);
15+
}
16+
17+
export function unserialize(envs: string[]): EnvVariables {
18+
return envs.reduce((acc, curr) => {
19+
const splitted = curr.split('=');
20+
acc = Object.assign({}, acc, { [splitted[0]]: splitted[1] });
21+
return acc;
22+
}, {});
23+
}
24+
25+
export function generate(data: any): EnvVariables {
26+
const envs = init();
27+
const request = data.requestData;
28+
const commit = request.data.pull_request && request.data.pull_request.head
29+
&& request.data.pull_request.head.sha ||
30+
request.data.head_commit && request.data.head_commit.id ||
31+
request.data.sha ||
32+
request.data.object_attributes && request.data.object_attributes.last_commit &&
33+
request.data.object_attributes.last_commit.id ||
34+
request.data.push && request.data.push.changes[0].commits[0].hash ||
35+
request.data.pullrequest && request.data.pullrequest.source &&
36+
request.data.pullrequest.source.commit &&
37+
request.data.pullrequest.source.commit.hash ||
38+
request.data.commit || '';
39+
const tag = request.ref && request.ref.startsWith('refs/tags/') ?
40+
request.ref.replace('refs/tags/', '') : null;
41+
42+
set(envs, 'ABSTRUSE_BRANCH', request.branch);
43+
set(envs, 'ABSTRUSE_BUILD_ID', data.build_id);
44+
set(envs, 'ABSTRUSE_JOB_ID', data.job_id);
45+
set(envs, 'ABSTRUSE_COMMIT', commit);
46+
set(envs, 'ABSTRUSE_EVENT_TYPE', request.pr ? 'pull_request' : 'push');
47+
set(envs, 'ABSTRUSE_PULL_REQUEST', request.pr ? request.pr : false);
48+
set(envs, 'ABSTRUSE_PULL_REQUEST_BRANCH', request.pr ? request.branch : '');
49+
set(envs, 'ABSTRUSE_TAG', tag);
50+
51+
const prSha = request.pr ? commit : '';
52+
set(envs, 'ABSTRUSE_PULL_REQUEST_SHA', prSha);
53+
54+
return envs;
55+
}
56+
57+
function init(): EnvVariables {
58+
return [
59+
'ABSTRUSE_BRANCH', 'ABSTRUSE_BUILD_DIR', 'ABSTRUSE_BUILD_ID',
60+
'ABSTRUSE_JOB_ID', 'ABSTRUSE_COMMIT', 'ABSTRUSE_EVENT_TYPE',
61+
'ABSTRUSE_PULL_REQUEST', 'ABSTRUSE_PULL_REQUEST_BRANCH',
62+
'ABSTRUSE_TAG', 'ABSTRUSE_PULL_REQEUST_SHA', 'ABSTRUSE_SECURE_ENV_VARS',
63+
'ABSTRUSE_TEST_RESULT'
64+
].reduce((acc, curr) => {
65+
acc = Object.assign(acc, { [curr]: null });
66+
return acc;
67+
}, {});
68+
}

src/api/process-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface JobProcess {
4040
status?: 'queued' | 'running' | 'cancelled' | 'errored' | 'success';
4141
image_name?: string;
4242
log?: string;
43+
requestData: any;
4344
commands?: { command: string, type: CommandType }[];
4445
cache?: string[];
4546
repo_name?: string;
@@ -564,14 +565,19 @@ export function stopBuild(buildId: number): Promise<any> {
564565

565566
function queueJob(jobId: number): Promise<void> {
566567
let job = null;
568+
let requestData = null;
569+
567570
return dbJob.getJob(jobId)
568571
.then(jobData => job = jobData)
572+
.then(() => getBuild(job.builds_id))
573+
.then(build => requestData = { branch: build.branch, pr: build.pr, data: build.data })
569574
.then(() => {
570575
const data = JSON.parse(job.data);
571576
const jobProcess: JobProcess = {
572577
build_id: job.builds_id,
573578
job_id: jobId,
574579
status: 'queued',
580+
requestData: requestData,
575581
commands: data.commands,
576582
cache: data.cache || null,
577583
repo_name: job.build.repository.full_name || null,

src/api/process.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getRepositoryByBuildId } from './db/repository';
66
import { Observable } from 'rxjs';
77
import { CommandType, Command, CommandTypePriority } from './config';
88
import { JobProcess } from './process-manager';
9+
import * as envVars from './env-variables';
910
import chalk from 'chalk';
1011
import * as style from 'ansi-styles';
1112
import { deploy } from './deploy';
@@ -25,7 +26,8 @@ export interface SpawnedProcessOutput {
2526
}
2627

2728
export interface ProcessOutput {
28-
type: 'data' | 'exit' | 'container' | 'exposed ports' | 'containerInfo' | 'containerError';
29+
type: 'data' | 'exit' | 'container' | 'exposed ports' | 'containerInfo' | 'containerError' |
30+
'env';
2931
data: any;
3032
}
3133

@@ -37,9 +39,10 @@ export function startBuildProcess(
3739
): Observable<ProcessOutput> {
3840
return new Observable(observer => {
3941
const image = proc.image_name;
40-
4142
const name = 'abstruse_' + proc.build_id + '_' + proc.job_id;
42-
const envs = proc.commands.filter(cmd => {
43+
44+
let envs = envVars.generate(proc);
45+
const initEnvs = proc.commands.filter(cmd => {
4346
return typeof cmd.command === 'string' && cmd.command.startsWith('export');
4447
})
4548
.map(cmd => cmd.command.replace('export', ''))
@@ -51,7 +54,9 @@ export function startBuildProcess(
5154
const gitTypes = [CommandType.git];
5255
const installTypes = [CommandType.before_install, CommandType.install];
5356
const scriptTypes = [CommandType.before_script, CommandType.script,
54-
CommandType.after_success, CommandType.after_failure, CommandType.after_script];
57+
CommandType.after_success, CommandType.after_failure,
58+
CommandType.after_script];
59+
5560
const gitCommands = prepareCommands(proc, gitTypes);
5661
const installCommands = prepareCommands(proc, installTypes);
5762
const scriptCommands = prepareCommands(proc, scriptTypes);
@@ -83,7 +88,7 @@ export function startBuildProcess(
8388

8489
restoreCache = Observable.concat(...[
8590
executeOutsideContainer(copyRestoreCmd),
86-
docker.dockerExec(name, { command: restoreCmd, type: CommandType.restore_cache })
91+
docker.dockerExec(name, { command: restoreCmd, type: CommandType.restore_cache, env: envs })
8792
]);
8893

8994
let cacheFolders = proc.cache.map(folder => {
@@ -104,27 +109,31 @@ export function startBuildProcess(
104109
].join('');
105110

106111
saveCache = Observable.concat(...[
107-
docker.dockerExec(name, { command: tarCmd, type: CommandType.store_cache }),
112+
docker.dockerExec(name, { command: tarCmd, type: CommandType.store_cache, env: envs }),
108113
executeOutsideContainer(saveTarCmd)
109114
]);
110115
}
111116

112-
const sub = docker.createContainer(name, image, envs)
113-
.concat(...gitCommands.map(cmd => docker.dockerExec(name, cmd)))
117+
const sub = docker.createContainer(name, image, initEnvs)
118+
.concat(...gitCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
114119
.concat(restoreCache)
115-
.concat(...installCommands.map(cmd => docker.dockerExec(name, cmd)))
120+
.concat(...installCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
116121
.concat(saveCache)
117-
.concat(...scriptCommands.map(cmd => docker.dockerExec(name, cmd)))
118-
.concat(...beforeDeployCommands.map(cmd => docker.dockerExec(name, cmd)))
119-
.concat(...deployCommands.map(cmd => docker.dockerExec(name, cmd)))
120-
.concat(deploy(deployPreferences, name, envs))
121-
.concat(...afterDeployCommands.map(cmd => docker.dockerExec(name, cmd)))
122+
.concat(...scriptCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
123+
.concat(...beforeDeployCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
124+
.concat(...deployCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
125+
.concat(deploy(deployPreferences, name, initEnvs))
126+
.concat(...afterDeployCommands.map(cmd => docker.dockerExec(name, cmd, envs)))
122127
.timeoutWith(idleTimeout, Observable.throw(new Error('command timeout')))
123128
.takeUntil(Observable.timer(jobTimeout).timeInterval().mergeMap(() => {
124129
return Observable.throw('job timeout');
125130
}))
126131
.subscribe((event: ProcessOutput) => {
127-
if (event.type === 'containerError') {
132+
if (event.type === 'env') {
133+
if (Object.keys(event.data).length) {
134+
envs = event.data;
135+
}
136+
} else if (event.type === 'containerError') {
128137
const msg = chalk.red((event.data.json && event.data.json.message) || event.data);
129138
observer.next({ type: 'exit', data: msg });
130139
observer.error(msg);
@@ -140,7 +149,7 @@ export function startBuildProcess(
140149
`last executed command exited with code ${event.data}`
141150
].join(' ');
142151
const tmsg = style.red.open + style.bold.open +
143-
`[error]: executed command returned exit code ${event.data}` +
152+
`\r\n[error]: executed command returned exit code ${event.data}` +
144153
style.bold.close + style.red.close;
145154
observer.next({ type: 'exit', data: chalk.red(tmsg) });
146155
observer.error(msg);
@@ -164,8 +173,8 @@ export function startBuildProcess(
164173
.catch(err => console.error(err));
165174
}, () => {
166175
const msg = style.green.open + style.bold.open +
167-
'[success]: build returned exit code 0' +
168-
style.bold.close + style.green.close;
176+
'\r\n[success]: build returned exit code 0' +
177+
style.bold.close + style.green.close;
169178
observer.next({ type: 'exit', data: chalk.green(msg) });
170179
docker.killContainer(name)
171180
.then(() => {

0 commit comments

Comments
 (0)