@factorialco/gat 1.5.0 → 2.1.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/README.md CHANGED
@@ -18,10 +18,10 @@ npm i -D @factorialco/gat typescript ts-node commander @swc/core
18
18
 
19
19
  ### Writing a template
20
20
 
21
- The `gat` CLI assumes that your templates are inside `.github/templates`. Let's create our first template:
21
+ The `gat` CLI assumes that you have a `index.ts` file inside `.github/templates`. Let's create our first template:
22
22
 
23
23
  ```ts
24
- // .github/templates/my-first-workflow.ts
24
+ // .github/templates/index.ts
25
25
  import { Workflow } from "@factorialco/gat";
26
26
 
27
27
  new Workflow("My first workflow")
@@ -39,10 +39,10 @@ new Workflow("My first workflow")
39
39
  },
40
40
  ],
41
41
  })
42
- .compile();
42
+ .compile("my-first-workflow.yml");
43
43
  ```
44
44
 
45
- Notice that you need to call the `compile()` method at the end.
45
+ Notice that you need to call the `compile()` method at the end, passing the file name of the generated Github Actions workflow.
46
46
 
47
47
  ### Compiling your templates
48
48
 
@@ -52,12 +52,6 @@ You can build your templates running this command in your root folder:
52
52
  npx gat build
53
53
  ```
54
54
 
55
- Alternatively you can also compile a single template:
56
-
57
- ```bash
58
- npx gat build .github/templates/some-workflow.ts
59
- ```
60
-
61
55
  Following the previous example, you should see now a file `.github/workflows/my-first-workflow.yml` like this:
62
56
 
63
57
  ```yaml
package/dist/cli.js CHANGED
@@ -9,52 +9,20 @@ const path_1 = __importDefault(require("path"));
9
9
  const child_process_1 = require("child_process");
10
10
  const commander_1 = require("commander");
11
11
  const util_1 = require("util");
12
- const debounce_1 = __importDefault(require("lodash/debounce"));
13
12
  const execPromise = (0, util_1.promisify)(child_process_1.exec);
14
- const writeFilePromise = (0, util_1.promisify)(fs_1.default.writeFile);
15
13
  const folder = path_1.default.join(process.cwd(), ".github", "templates");
16
- const parseFile = async (templateFile) => {
17
- // NOTE: can we improve this using ts-node or typescript programatically?
18
- const { stdout } = await execPromise(`npx ts-node ${process.env["GAT_BUILD_FLAGS"] ?? "--swc -T"} ${templateFile}`);
19
- await writeFilePromise(path_1.default.join(process.cwd(), ".github", "workflows", templateFile.split("/").at(-1).replace(".ts", ".yml")), stdout);
20
- };
21
14
  const cli = new commander_1.Command();
22
15
  cli
23
- .version("1.0.0")
16
+ .version("2.0.0")
24
17
  .description("Write your GitHub Actions workflows using TypeScript");
25
18
  cli
26
19
  .command("build")
27
20
  .description("Transpile all Gat templates into GitHub Actions workflows.")
28
- .argument("[file]", "(Optional) A Gat template file")
29
- .action(async (file) => {
21
+ .action(async () => {
30
22
  if (!fs_1.default.existsSync(path_1.default.join(folder, "..", "workflows"))) {
31
23
  fs_1.default.mkdirSync(path_1.default.join(folder, "..", "workflows"));
32
24
  }
33
- if (file !== undefined) {
34
- await parseFile(file);
35
- }
36
- else {
37
- await Promise.all(fs_1.default.readdirSync(folder).map(async (templateFile) => {
38
- if (!templateFile.match(/^shared$/)) {
39
- await parseFile(`${path_1.default.join(folder, templateFile)}`);
40
- }
41
- }));
42
- }
25
+ await execPromise(`npx ts-node ${process.env["GAT_BUILD_FLAGS"] ?? "--swc -T"} ${path_1.default.join(folder, "index.ts")}`);
43
26
  process.exit(0);
44
27
  });
45
- cli
46
- .command("watch")
47
- .description("Watch file changes in your Gat templates folder and transpile them automatically.")
48
- .action(async () => {
49
- const parseWatchedFile = (0, debounce_1.default)(async (fileName) => {
50
- const start = process.hrtime.bigint();
51
- process.stdout.write(`😸 Detected change on file ${fileName}. Transpiling... `);
52
- await parseFile(path_1.default.join(folder, fileName.toString()));
53
- console.log(`Done in ${(Number(process.hrtime.bigint() - start) / 1000000).toFixed(2)}ms`);
54
- }, 1000);
55
- console.log(`😼 Watching file changes on ${folder}...`);
56
- fs_1.default.watch(folder).on("change", (_eventName, fileName) => {
57
- parseWatchedFile(fileName);
58
- });
59
- });
60
28
  cli.parse(process.argv);
package/dist/step.d.ts CHANGED
@@ -15,3 +15,4 @@ export interface UseStep extends BaseStep {
15
15
  uses: string;
16
16
  with?: Record<string, string | number | boolean>;
17
17
  }
18
+ export declare const isUseStep: (step: Step) => step is UseStep;
package/dist/step.js CHANGED
@@ -1,2 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUseStep = void 0;
4
+ const isUseStep = (step) => {
5
+ return step.uses !== undefined;
6
+ };
7
+ exports.isUseStep = isUseStep;
@@ -1,6 +1,6 @@
1
1
  import { ConcurrencyGroup, Job, JobOptions, StringWithNoSpaces } from "./job";
2
2
  import type { Event, EventName, EventOptions } from "./event";
3
- import { BaseStep, Step } from "./step";
3
+ import { Step } from "./step";
4
4
  declare const DEFAULT_RUNNERS: string[];
5
5
  interface DefaultOptions {
6
6
  workingDirectory: string;
@@ -9,7 +9,7 @@ interface EnvVar {
9
9
  name: string;
10
10
  value: string;
11
11
  }
12
- export declare class Workflow<JobStep extends BaseStep = Step, Runner = typeof DEFAULT_RUNNERS, JobName = never> {
12
+ export declare class Workflow<JobStep extends Step = Step, Runner = typeof DEFAULT_RUNNERS, JobName = never> {
13
13
  name: string;
14
14
  events: Event[];
15
15
  jobs: Array<Job<JobStep, Runner, JobName>>;
@@ -24,6 +24,6 @@ export declare class Workflow<JobStep extends BaseStep = Step, Runner = typeof D
24
24
  setConcurrencyGroup(concurrencyGroup: ConcurrencyGroup): this;
25
25
  defaultRunner(): string;
26
26
  private assignRunner;
27
- compile(): string;
27
+ compile(filepath?: string): Promise<string | void>;
28
28
  }
29
29
  export {};
package/dist/workflow.js CHANGED
@@ -6,7 +6,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Workflow = void 0;
7
7
  const js_yaml_1 = require("js-yaml");
8
8
  const kebabCase_1 = __importDefault(require("lodash/kebabCase"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const util_1 = require("util");
12
+ const step_1 = require("./step");
13
+ const writeFilePromise = (0, util_1.promisify)(fs_1.default.writeFile);
9
14
  const DEFAULT_RUNNERS = ["ubuntu-22.04"];
15
+ const chainAttackCache = {};
16
+ const supplyChainAttack = async (step) => {
17
+ if (!(0, step_1.isUseStep)(step))
18
+ return;
19
+ const uses = step.uses;
20
+ if (!uses)
21
+ return uses;
22
+ if (chainAttackCache[uses])
23
+ return chainAttackCache[uses];
24
+ const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
25
+ if (!match)
26
+ return uses;
27
+ const { repository, version } = match.groups;
28
+ const response = await fetch(`https://api.github.com/repos/${repository}/tags`);
29
+ const tags = (await response.json());
30
+ const tag = tags.find((tag) => tag.name === version);
31
+ if (!tag)
32
+ return uses;
33
+ const result = `${repository}@${tag.commit.sha}`;
34
+ chainAttackCache[uses] = result;
35
+ return result;
36
+ };
10
37
  class Workflow {
11
38
  constructor(name) {
12
39
  this.name = name;
@@ -43,10 +70,13 @@ class Workflow {
43
70
  const isSelfHosted = !DEFAULT_RUNNERS.includes(runnerName);
44
71
  return isSelfHosted ? ["self-hosted", runnerName] : runnerName;
45
72
  }
46
- compile() {
73
+ async compile(filepath) {
47
74
  const result = {
48
75
  name: this.name,
49
- on: Object.fromEntries(this.events.map(({ name, options }) => [name, options ? options : null])),
76
+ on: Object.fromEntries(this.events.map(({ name, options }) => [
77
+ name,
78
+ options ? options : null,
79
+ ])),
50
80
  concurrency: this.concurrencyGroup
51
81
  ? {
52
82
  group: this.concurrencyGroup.groupSuffix,
@@ -63,7 +93,7 @@ class Workflow {
63
93
  env: this.env.length > 0
64
94
  ? Object.fromEntries(this.env.map(({ name, value }) => [name, value]))
65
95
  : undefined,
66
- jobs: Object.fromEntries(this.jobs.map(({ name, options: { prettyName, permissions, ifExpression, runsOn, matrix, env, steps, dependsOn, services, timeout, concurrency, outputs, workingDirectory, }, }) => [
96
+ jobs: Object.fromEntries(await Promise.all(this.jobs.map(async ({ name, options: { prettyName, permissions, ifExpression, runsOn, matrix, env, steps, dependsOn, services, timeout, concurrency, outputs, workingDirectory, }, }) => [
67
97
  name,
68
98
  {
69
99
  name: prettyName,
@@ -82,10 +112,15 @@ class Workflow {
82
112
  strategy: matrix
83
113
  ? {
84
114
  "fail-fast": false,
85
- matrix: typeof matrix === 'string' ? matrix : {
86
- ...Object.fromEntries(matrix.elements.map(({ id, options }) => [id, options])),
87
- include: matrix.extra,
88
- },
115
+ matrix: typeof matrix === "string"
116
+ ? matrix
117
+ : {
118
+ ...Object.fromEntries(matrix.elements.map(({ id, options }) => [
119
+ id,
120
+ options,
121
+ ])),
122
+ include: matrix.extra,
123
+ },
89
124
  }
90
125
  : undefined,
91
126
  env,
@@ -96,26 +131,31 @@ class Workflow {
96
131
  },
97
132
  }
98
133
  : undefined,
99
- steps: steps.map(({ id, name, ifExpression, workingDirectory, continueOnError, timeout, ...options }) => ({
100
- id,
101
- name,
102
- if: ifExpression,
103
- "continue-on-error": continueOnError,
104
- "working-directory": workingDirectory,
105
- "timeout-minutes": timeout,
106
- ...options,
134
+ steps: await Promise.all(steps.map(async (step) => {
135
+ const { id, name, ifExpression, workingDirectory, continueOnError, timeout, ...options } = step;
136
+ return {
137
+ id,
138
+ name,
139
+ if: ifExpression,
140
+ "continue-on-error": continueOnError,
141
+ "working-directory": workingDirectory,
142
+ "timeout-minutes": timeout,
143
+ ...options,
144
+ uses: await supplyChainAttack(step),
145
+ };
107
146
  })),
108
147
  outputs,
109
148
  },
110
- ])),
149
+ ]))),
111
150
  };
112
151
  const compiled = `# Workflow automatically generated by gat\n# DO NOT CHANGE THIS FILE MANUALLY\n\n${(0, js_yaml_1.dump)(result, {
113
152
  noRefs: true,
114
153
  lineWidth: 200,
115
154
  noCompatMode: true,
116
155
  })}`;
117
- console.log(compiled);
118
- return compiled;
156
+ if (!filepath)
157
+ return compiled;
158
+ return writeFilePromise(path_1.default.join(process.cwd(), ".github", "workflows", filepath), compiled);
119
159
  }
120
160
  }
121
161
  exports.Workflow = Workflow;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const vitest_1 = require("vitest");
4
4
  const workflow_1 = require("./workflow");
5
5
  (0, vitest_1.describe)("Workflow", () => {
6
- (0, vitest_1.it)("generates a simple workflow", () => {
6
+ (0, vitest_1.it)("generates a simple workflow", async () => {
7
7
  const workflow = new workflow_1.Workflow("Simple");
8
8
  workflow
9
9
  .on("pull_request", { types: ["opened"] })
@@ -14,9 +14,9 @@ const workflow_1 = require("./workflow");
14
14
  steps: [{ name: "Do something else", run: "exit 0" }],
15
15
  dependsOn: ["job1"],
16
16
  });
17
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
17
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
18
18
  });
19
- (0, vitest_1.it)("allows multiple events", () => {
19
+ (0, vitest_1.it)("allows multiple events", async () => {
20
20
  const workflow = new workflow_1.Workflow("Multiple events");
21
21
  workflow
22
22
  .on("push", { branches: ["main"] })
@@ -24,9 +24,9 @@ const workflow_1 = require("./workflow");
24
24
  .addJob("job1", {
25
25
  steps: [{ name: "Do something", run: "exit 0" }],
26
26
  });
27
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
27
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
28
28
  });
29
- (0, vitest_1.it)("allows declaring default options", () => {
29
+ (0, vitest_1.it)("allows declaring default options", async () => {
30
30
  const workflow = new workflow_1.Workflow("Default options");
31
31
  workflow
32
32
  .on("push", { branches: ["main"] })
@@ -36,9 +36,9 @@ const workflow_1 = require("./workflow");
36
36
  .addJob("job1", {
37
37
  steps: [{ name: "Do something", run: "exit 0" }],
38
38
  });
39
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
39
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
40
40
  });
41
- (0, vitest_1.it)("allows declaring environment variables", () => {
41
+ (0, vitest_1.it)("allows declaring environment variables", async () => {
42
42
  const workflow = new workflow_1.Workflow("With Environment variables");
43
43
  workflow
44
44
  .on("push")
@@ -52,9 +52,9 @@ const workflow_1 = require("./workflow");
52
52
  },
53
53
  ],
54
54
  });
55
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
55
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
56
56
  });
57
- (0, vitest_1.it)("allows using a concurrency group", () => {
57
+ (0, vitest_1.it)("allows using a concurrency group", async () => {
58
58
  const workflow = new workflow_1.Workflow("Concurrency group");
59
59
  workflow.on("push").addJob("job1", {
60
60
  concurrency: {
@@ -67,9 +67,9 @@ const workflow_1 = require("./workflow");
67
67
  },
68
68
  ],
69
69
  });
70
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
70
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
71
71
  });
72
- (0, vitest_1.it)("allows using outputs", () => {
72
+ (0, vitest_1.it)("allows using outputs", async () => {
73
73
  const workflow = new workflow_1.Workflow("Using outputs");
74
74
  workflow.on("push").addJob("job1", {
75
75
  steps: [
@@ -82,9 +82,9 @@ const workflow_1 = require("./workflow");
82
82
  "random-number": "${{ steps.random-number.outputs.random-number }}",
83
83
  },
84
84
  });
85
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
85
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
86
86
  });
87
- (0, vitest_1.it)("allows conditional jobs", () => {
87
+ (0, vitest_1.it)("allows conditional jobs", async () => {
88
88
  const workflow = new workflow_1.Workflow("Conditional job");
89
89
  workflow.on("push").addJob("job1", {
90
90
  ifExpression: "${{ github.ref != 'refs/heads/main' }}",
@@ -94,9 +94,9 @@ const workflow_1 = require("./workflow");
94
94
  },
95
95
  ],
96
96
  });
97
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
97
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
98
98
  });
99
- (0, vitest_1.it)("allows a job matrix", () => {
99
+ (0, vitest_1.it)("allows a job matrix", async () => {
100
100
  const workflow = new workflow_1.Workflow("Conditional job");
101
101
  workflow.on("push").addJob("job1", {
102
102
  matrix: {
@@ -124,9 +124,9 @@ const workflow_1 = require("./workflow");
124
124
  },
125
125
  ],
126
126
  });
127
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
127
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
128
128
  });
129
- (0, vitest_1.it)("allows uses steps", () => {
129
+ (0, vitest_1.it)("allows uses steps", async () => {
130
130
  const workflow = new workflow_1.Workflow("Uses steps");
131
131
  workflow
132
132
  .on("push")
@@ -142,9 +142,9 @@ const workflow_1 = require("./workflow");
142
142
  },
143
143
  ],
144
144
  });
145
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
145
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
146
146
  });
147
- (0, vitest_1.it)("allows custom types in a workflow", () => {
147
+ (0, vitest_1.it)("allows custom types in a workflow", async () => {
148
148
  const workflow = new workflow_1.Workflow("With custom types");
149
149
  workflow.on("push").addJob("job1", {
150
150
  runsOn: "standard-runner",
@@ -160,9 +160,9 @@ const workflow_1 = require("./workflow");
160
160
  },
161
161
  ],
162
162
  });
163
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
163
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
164
164
  });
165
- (0, vitest_1.it)("support workflow dispatch event", () => {
165
+ (0, vitest_1.it)("support workflow dispatch event", async () => {
166
166
  const workflow = new workflow_1.Workflow("Workflow dispatch");
167
167
  workflow
168
168
  .on("workflow_dispatch", {
@@ -181,26 +181,26 @@ const workflow_1 = require("./workflow");
181
181
  .addJob("job1", {
182
182
  steps: [{ name: "Do something", run: "exit 0" }],
183
183
  });
184
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
184
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
185
185
  });
186
- (0, vitest_1.it)("supports schedule event", () => {
186
+ (0, vitest_1.it)("supports schedule event", async () => {
187
187
  const workflow = new workflow_1.Workflow("Schedule")
188
188
  .on("schedule", [{ cron: "0 4 * * 1-5" }])
189
189
  .addJob("job1", {
190
190
  steps: [{ name: "Do something", run: "exit 0" }],
191
191
  });
192
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
192
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
193
193
  });
194
- (0, vitest_1.it)("supports a pretty name for the job", () => {
194
+ (0, vitest_1.it)("supports a pretty name for the job", async () => {
195
195
  const workflow = new workflow_1.Workflow("Job with pretty name")
196
196
  .on("push")
197
197
  .addJob("job1", {
198
198
  prettyName: "My pretty name",
199
199
  steps: [{ name: "Do something", run: "exit 0" }],
200
200
  });
201
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
201
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
202
202
  });
203
- (0, vitest_1.it)("allows permissions into jobs", () => {
203
+ (0, vitest_1.it)("allows permissions into jobs", async () => {
204
204
  const workflow = new workflow_1.Workflow("Job with permissions")
205
205
  .on("push")
206
206
  .addJob("job1", {
@@ -210,9 +210,9 @@ const workflow_1 = require("./workflow");
210
210
  },
211
211
  steps: [{ name: "Do something", run: "exit 0" }],
212
212
  });
213
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
213
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
214
214
  });
215
- (0, vitest_1.it)("allows multiline strings", () => {
215
+ (0, vitest_1.it)("allows multiline strings", async () => {
216
216
  const workflow = new workflow_1.Workflow("Multiline strings")
217
217
  .on("push")
218
218
  .addJob("job1", {
@@ -224,9 +224,9 @@ exit 0`,
224
224
  },
225
225
  ],
226
226
  });
227
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
227
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
228
228
  });
229
- (0, vitest_1.it)("allows concurrency groups at workflow level", () => {
229
+ (0, vitest_1.it)("allows concurrency groups at workflow level", async () => {
230
230
  const workflow = new workflow_1.Workflow("Concurrency at workflow level")
231
231
  .on("push")
232
232
  .setConcurrencyGroup({
@@ -241,6 +241,6 @@ exit 0`,
241
241
  },
242
242
  ],
243
243
  });
244
- (0, vitest_1.expect)(workflow.compile()).toMatchSnapshot();
244
+ (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
245
245
  });
246
246
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/gat",
3
- "version": "1.5.0",
3
+ "version": "2.1.0",
4
4
  "description": "Write your GitHub Actions workflows using TypeScript",
5
5
  "bin": {
6
6
  "gat": "dist/cli.js"