@factorialco/gat 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/job.d.ts +6 -5
- package/dist/workflow.d.ts +7 -2
- package/dist/workflow.js +63 -54
- package/dist/workflow.spec.js +31 -0
- package/package.json +2 -2
package/dist/job.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface Service {
|
|
|
20
20
|
options?: string;
|
|
21
21
|
volumes?: string[];
|
|
22
22
|
}
|
|
23
|
-
export interface
|
|
23
|
+
export interface StepsJobOptions<Step, RunnerDefinition, Name> {
|
|
24
24
|
prettyName?: string;
|
|
25
25
|
permissions?: object;
|
|
26
26
|
ifExpression?: string;
|
|
@@ -33,11 +33,12 @@ export interface BaseJobOptions<RunnerDefinition, Name> {
|
|
|
33
33
|
matrix?: Matrix | string;
|
|
34
34
|
outputs?: Record<string, string>;
|
|
35
35
|
workingDirectory?: string;
|
|
36
|
-
}
|
|
37
|
-
export interface StepsJobOptions<Step, RunnerDefinition, Name> extends BaseJobOptions<RunnerDefinition, Name> {
|
|
38
36
|
steps: Step[];
|
|
39
37
|
}
|
|
40
|
-
export interface UsesJobOptions<
|
|
38
|
+
export interface UsesJobOptions<Name> {
|
|
39
|
+
prettyName?: string;
|
|
40
|
+
dependsOn?: Array<Name>;
|
|
41
|
+
ifExpression?: string;
|
|
41
42
|
uses: string;
|
|
42
43
|
with?: Record<string, string | number | boolean | object>;
|
|
43
44
|
secrets?: Record<string, string | number | boolean | object> | "inherit";
|
|
@@ -45,5 +46,5 @@ export interface UsesJobOptions<RunnerDefinition, Name> extends BaseJobOptions<R
|
|
|
45
46
|
export type StringWithNoSpaces<T> = T extends `${string} ${string}` ? never : T extends ` ${string}` ? never : T extends `${string} ` ? never : T;
|
|
46
47
|
export interface Job<Step, RunnerDefinition, Name> {
|
|
47
48
|
name: string;
|
|
48
|
-
options: StepsJobOptions<Step, RunnerDefinition, Name> | UsesJobOptions<
|
|
49
|
+
options: StepsJobOptions<Step, RunnerDefinition, Name> | UsesJobOptions<Name>;
|
|
49
50
|
}
|
package/dist/workflow.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ interface EnvVar {
|
|
|
8
8
|
name: string;
|
|
9
9
|
value: string;
|
|
10
10
|
}
|
|
11
|
+
interface CompileOptions {
|
|
12
|
+
filePath?: string;
|
|
13
|
+
lockFilePath?: string;
|
|
14
|
+
writeLockFile?: boolean;
|
|
15
|
+
}
|
|
11
16
|
export type RunnerDefinition = string | {
|
|
12
17
|
group: string;
|
|
13
18
|
labels?: string[];
|
|
@@ -22,10 +27,10 @@ export declare class Workflow<JobStep extends Step = Step, Runner extends Runner
|
|
|
22
27
|
constructor(name: string);
|
|
23
28
|
on<T extends EventName>(name: T, options?: EventOptions<T>): this;
|
|
24
29
|
addDefaults(options: DefaultOptions): this;
|
|
25
|
-
addJob<T extends string>(name: StringWithNoSpaces<T>, options: StepsJobOptions<JobStep, Runner, JobName> | UsesJobOptions<
|
|
30
|
+
addJob<T extends string>(name: StringWithNoSpaces<T>, options: StepsJobOptions<JobStep, Runner, JobName> | UsesJobOptions<JobName>): Workflow<JobStep, Runner, JobName | T>;
|
|
26
31
|
setEnv(name: string, value: string): this;
|
|
27
32
|
setConcurrencyGroup(concurrencyGroup: ConcurrencyGroup | null): this;
|
|
28
33
|
defaultRunner(): string;
|
|
29
|
-
compile(
|
|
34
|
+
compile(compileOptions?: CompileOptions): Promise<string | void>;
|
|
30
35
|
}
|
|
31
36
|
export {};
|
package/dist/workflow.js
CHANGED
|
@@ -12,28 +12,38 @@ const util_1 = require("util");
|
|
|
12
12
|
const axios_1 = __importDefault(require("axios"));
|
|
13
13
|
const step_1 = require("./step");
|
|
14
14
|
const writeFilePromise = (0, util_1.promisify)(fs_1.default.writeFile);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const supplyChainAttack = async (step, compileOptions) => {
|
|
16
|
+
const { lockFilePath, writeLockFile = false } = compileOptions;
|
|
17
17
|
if (!(0, step_1.isUseStep)(step))
|
|
18
18
|
return;
|
|
19
|
-
|
|
19
|
+
// The user is not interested in frozen sha versions
|
|
20
|
+
if (!lockFilePath)
|
|
20
21
|
return step.uses;
|
|
21
22
|
const uses = step.uses;
|
|
22
|
-
if (!uses)
|
|
23
|
-
return uses;
|
|
24
|
-
if (chainAttackCache[uses])
|
|
25
|
-
return chainAttackCache[uses];
|
|
26
23
|
const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
|
|
24
|
+
// The uses is not a valid Github action with a tag
|
|
27
25
|
if (!match)
|
|
28
26
|
return uses;
|
|
29
27
|
const { repository, version } = match.groups;
|
|
28
|
+
const chainAttackCache = fs_1.default.existsSync(lockFilePath)
|
|
29
|
+
? JSON.parse(fs_1.default.readFileSync(lockFilePath, "utf8"))
|
|
30
|
+
: {};
|
|
31
|
+
if (chainAttackCache[uses])
|
|
32
|
+
return chainAttackCache[uses];
|
|
33
|
+
if (!writeLockFile) {
|
|
34
|
+
throw new Error(`Unable to retrieve ${uses} from lock file and writeLockFile is false`);
|
|
35
|
+
}
|
|
30
36
|
const response = await axios_1.default.get(`https://api.github.com/repos/${repository}/tags`);
|
|
31
37
|
const tags = response.data;
|
|
32
38
|
const tag = tags.find((tag) => tag.name === version);
|
|
33
|
-
if (!tag)
|
|
34
|
-
|
|
39
|
+
if (!tag) {
|
|
40
|
+
throw new Error(`Unable to retrieve ${uses} from Github tags`);
|
|
41
|
+
}
|
|
35
42
|
const result = `${repository}@${tag.commit.sha}`;
|
|
36
43
|
chainAttackCache[uses] = result;
|
|
44
|
+
if (writeLockFile) {
|
|
45
|
+
fs_1.default.writeFileSync(lockFilePath, JSON.stringify(chainAttackCache, null, 2));
|
|
46
|
+
}
|
|
37
47
|
return result;
|
|
38
48
|
};
|
|
39
49
|
class Workflow {
|
|
@@ -67,7 +77,7 @@ class Workflow {
|
|
|
67
77
|
defaultRunner() {
|
|
68
78
|
return "ubuntu-22.04";
|
|
69
79
|
}
|
|
70
|
-
async compile(
|
|
80
|
+
async compile(compileOptions = {}) {
|
|
71
81
|
const result = {
|
|
72
82
|
name: this.name,
|
|
73
83
|
on: Object.fromEntries(this.events.map(({ name, options }) => [
|
|
@@ -91,60 +101,59 @@ class Workflow {
|
|
|
91
101
|
? Object.fromEntries(this.env.map(({ name, value }) => [name, value]))
|
|
92
102
|
: undefined,
|
|
93
103
|
jobs: Object.fromEntries(await Promise.all(this.jobs.map(async ({ name, options: jobOptions }) => {
|
|
94
|
-
const { prettyName, permissions, ifExpression, runsOn, matrix, env, dependsOn, services, timeout, concurrency, outputs, workingDirectory, } = jobOptions;
|
|
95
|
-
const computedOptions = {
|
|
96
|
-
name: prettyName,
|
|
97
|
-
permissions,
|
|
98
|
-
if: ifExpression,
|
|
99
|
-
"runs-on": runsOn ?? this.defaultRunner(),
|
|
100
|
-
"timeout-minutes": timeout ?? 15,
|
|
101
|
-
needs: dependsOn,
|
|
102
|
-
services,
|
|
103
|
-
concurrency: concurrency
|
|
104
|
-
? {
|
|
105
|
-
group: `${(0, kebabCase_1.default)(this.name)}-${name}-${concurrency.groupSuffix}`,
|
|
106
|
-
"cancel-in-progress": concurrency.cancelPrevious,
|
|
107
|
-
}
|
|
108
|
-
: undefined,
|
|
109
|
-
strategy: matrix
|
|
110
|
-
? {
|
|
111
|
-
"fail-fast": false,
|
|
112
|
-
matrix: typeof matrix === "string"
|
|
113
|
-
? matrix
|
|
114
|
-
: {
|
|
115
|
-
...Object.fromEntries(matrix.elements.map(({ id, options }) => [
|
|
116
|
-
id,
|
|
117
|
-
options,
|
|
118
|
-
])),
|
|
119
|
-
include: matrix.extra,
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
: undefined,
|
|
123
|
-
env,
|
|
124
|
-
defaults: workingDirectory
|
|
125
|
-
? {
|
|
126
|
-
run: {
|
|
127
|
-
"working-directory": workingDirectory,
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
: undefined,
|
|
131
|
-
};
|
|
132
104
|
if ("uses" in jobOptions) {
|
|
105
|
+
const { prettyName, ifExpression, dependsOn } = jobOptions;
|
|
133
106
|
return [
|
|
134
107
|
name,
|
|
135
108
|
{
|
|
136
|
-
|
|
109
|
+
name: prettyName,
|
|
110
|
+
if: ifExpression,
|
|
111
|
+
needs: dependsOn,
|
|
137
112
|
uses: jobOptions.uses,
|
|
138
113
|
with: jobOptions.with,
|
|
139
114
|
secrets: jobOptions.secrets,
|
|
140
|
-
outputs,
|
|
141
115
|
},
|
|
142
116
|
];
|
|
143
117
|
}
|
|
118
|
+
const { prettyName, permissions, ifExpression, runsOn, matrix, env, dependsOn, services, timeout, concurrency, outputs, workingDirectory, } = jobOptions;
|
|
144
119
|
return [
|
|
145
120
|
name,
|
|
146
121
|
{
|
|
147
|
-
|
|
122
|
+
name: prettyName,
|
|
123
|
+
permissions,
|
|
124
|
+
if: ifExpression,
|
|
125
|
+
"runs-on": runsOn ?? this.defaultRunner(),
|
|
126
|
+
"timeout-minutes": timeout ?? 15,
|
|
127
|
+
needs: dependsOn,
|
|
128
|
+
services,
|
|
129
|
+
concurrency: concurrency
|
|
130
|
+
? {
|
|
131
|
+
group: `${(0, kebabCase_1.default)(this.name)}-${name}-${concurrency.groupSuffix}`,
|
|
132
|
+
"cancel-in-progress": concurrency.cancelPrevious,
|
|
133
|
+
}
|
|
134
|
+
: undefined,
|
|
135
|
+
strategy: matrix
|
|
136
|
+
? {
|
|
137
|
+
"fail-fast": false,
|
|
138
|
+
matrix: typeof matrix === "string"
|
|
139
|
+
? matrix
|
|
140
|
+
: {
|
|
141
|
+
...Object.fromEntries(matrix.elements.map(({ id, options }) => [
|
|
142
|
+
id,
|
|
143
|
+
options,
|
|
144
|
+
])),
|
|
145
|
+
include: matrix.extra,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: undefined,
|
|
149
|
+
env,
|
|
150
|
+
defaults: workingDirectory
|
|
151
|
+
? {
|
|
152
|
+
run: {
|
|
153
|
+
"working-directory": workingDirectory,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
148
157
|
steps: await Promise.all(jobOptions.steps.map(async (step) => {
|
|
149
158
|
const { id, name, ifExpression, workingDirectory, continueOnError, timeout, ...options } = step;
|
|
150
159
|
return {
|
|
@@ -155,7 +164,7 @@ class Workflow {
|
|
|
155
164
|
"working-directory": workingDirectory,
|
|
156
165
|
"timeout-minutes": timeout,
|
|
157
166
|
...options,
|
|
158
|
-
uses: await supplyChainAttack(step),
|
|
167
|
+
uses: await supplyChainAttack(step, compileOptions),
|
|
159
168
|
};
|
|
160
169
|
})),
|
|
161
170
|
outputs,
|
|
@@ -168,9 +177,9 @@ class Workflow {
|
|
|
168
177
|
lineWidth: 200,
|
|
169
178
|
noCompatMode: true,
|
|
170
179
|
})}`;
|
|
171
|
-
if (!
|
|
180
|
+
if (!compileOptions.filePath)
|
|
172
181
|
return compiled;
|
|
173
|
-
return writeFilePromise(path_1.default.join(process.cwd(), ".github", "workflows",
|
|
182
|
+
return writeFilePromise(path_1.default.join(process.cwd(), ".github", "workflows", compileOptions.filePath), compiled);
|
|
174
183
|
}
|
|
175
184
|
}
|
|
176
185
|
exports.Workflow = Workflow;
|
package/dist/workflow.spec.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const vitest_1 = require("vitest");
|
|
4
7
|
const workflow_1 = require("./workflow");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
5
9
|
(0, vitest_1.describe)("Workflow", () => {
|
|
6
10
|
(0, vitest_1.it)("generates a simple workflow", async () => {
|
|
7
11
|
const workflow = new workflow_1.Workflow("Simple");
|
|
@@ -278,4 +282,31 @@ exit 0`,
|
|
|
278
282
|
});
|
|
279
283
|
(0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
|
|
280
284
|
});
|
|
285
|
+
(0, vitest_1.it)("supports supply chain attack, without writing the lock file", async () => {
|
|
286
|
+
const workflow = new workflow_1.Workflow("Supply chain attack");
|
|
287
|
+
workflow.on("push").addJob("job1", {
|
|
288
|
+
steps: [
|
|
289
|
+
{ name: "Do something", uses: "tj-actions/changed-files@v45.0.7" },
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
fs_1.default.writeFileSync("/tmp/lockfile1.json", JSON.stringify({
|
|
293
|
+
"tj-actions/changed-files@v45.0.7": "tj-actions/changed-files@youhavebeenhacked",
|
|
294
|
+
}, null, 2));
|
|
295
|
+
(0, vitest_1.expect)(await workflow.compile({
|
|
296
|
+
lockFilePath: "/tmp/lockfile1.json",
|
|
297
|
+
writeLockFile: false,
|
|
298
|
+
})).toMatchSnapshot();
|
|
299
|
+
});
|
|
300
|
+
(0, vitest_1.it)("supports supply chain attack, with writing the lock file", async () => {
|
|
301
|
+
const workflow = new workflow_1.Workflow("Supply chain attack");
|
|
302
|
+
workflow.on("push").addJob("job1", {
|
|
303
|
+
steps: [
|
|
304
|
+
{ name: "Do something", uses: "tj-actions/changed-files@v45.0.7" },
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
(0, vitest_1.expect)(await workflow.compile({
|
|
308
|
+
lockFilePath: "/tmp/lockfile2.json",
|
|
309
|
+
writeLockFile: true,
|
|
310
|
+
})).toMatchSnapshot();
|
|
311
|
+
});
|
|
281
312
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factorialco/gat",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Write your GitHub Actions workflows using TypeScript",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gat": "dist/cli.js"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"eslint": "^8.23.0",
|
|
34
34
|
"eslint-config-prettier": "^9.0.0",
|
|
35
35
|
"prettier": "^3.0.3",
|
|
36
|
-
"tsx": "^
|
|
36
|
+
"tsx": "^4.19.3",
|
|
37
37
|
"typescript": "^5.2.2",
|
|
38
38
|
"vitest": "^0.34.6"
|
|
39
39
|
},
|