@factorialco/gat 3.3.0 → 3.4.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.
@@ -0,0 +1,8 @@
1
+ import { Workflow } from "./workflow";
2
+ interface CompileOptions {
3
+ templates: Record<string, Workflow>;
4
+ lockFilePath: string;
5
+ writeLockFile: boolean;
6
+ }
7
+ export declare const compileTemplates: (options: CompileOptions) => Promise<(string | void)[]>;
8
+ export {};
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compileTemplates = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const rest_1 = require("@octokit/rest");
9
+ const createLockFile = async (templates, lockFilePath) => {
10
+ const actions = Object.values(templates).reduce((acc, template) => {
11
+ template.jobs.forEach((job) => {
12
+ if ("steps" in job.options) {
13
+ job.options.steps.forEach((step) => {
14
+ if ("uses" in step) {
15
+ acc.push(step.uses);
16
+ }
17
+ });
18
+ }
19
+ });
20
+ return acc;
21
+ }, []);
22
+ const uniqueActions = [...new Set(actions)];
23
+ const octokit = process.env.GITHUB_TOKEN
24
+ ? new rest_1.Octokit({
25
+ auth: process.env.GITHUB_TOKEN,
26
+ })
27
+ : new rest_1.Octokit();
28
+ const resolvedActions = {};
29
+ await Promise.all(uniqueActions.map(async (action) => {
30
+ const match = action.match(/(?<repository>.*)@(?<version>.*)/);
31
+ if (!match)
32
+ return;
33
+ const { repository, version } = match.groups;
34
+ const [owner, repo] = repository.split("/");
35
+ const response = await octokit.rest.repos.listTags({
36
+ owner,
37
+ repo,
38
+ });
39
+ const tag = response.data.find((tag) => tag.name === version);
40
+ if (!tag) {
41
+ throw new Error(`Unable to retrieve ${action} from Github tags`);
42
+ }
43
+ resolvedActions[action] = `${repository}@${tag.commit.sha}`;
44
+ }));
45
+ fs_1.default.writeFileSync(lockFilePath, JSON.stringify(resolvedActions, null, 2));
46
+ };
47
+ const compileTemplates = async (options) => {
48
+ const { templates, lockFilePath, writeLockFile } = options;
49
+ if (writeLockFile) {
50
+ await createLockFile(templates, lockFilePath);
51
+ }
52
+ const resolvedActions = fs_1.default.existsSync(lockFilePath)
53
+ ? JSON.parse(fs_1.default.readFileSync(lockFilePath, "utf8"))
54
+ : {};
55
+ return Promise.all(Object.entries(templates).map(([filePath, template]) => template.compile({ filePath, resolvedActions })));
56
+ };
57
+ exports.compileTemplates = compileTemplates;
@@ -1,5 +1,5 @@
1
- import { ConcurrencyGroup, Job, StepsJobOptions, StringWithNoSpaces, UsesJobOptions } from "./job";
2
1
  import type { Event, EventName, EventOptions } from "./event";
2
+ import { ConcurrencyGroup, Job, StepsJobOptions, StringWithNoSpaces, UsesJobOptions } from "./job";
3
3
  import { type Step } from "./step";
4
4
  interface DefaultOptions {
5
5
  workingDirectory: string;
@@ -8,10 +8,9 @@ interface EnvVar {
8
8
  name: string;
9
9
  value: string;
10
10
  }
11
- interface CompileOptions {
11
+ interface WorkflowCompileOptions {
12
12
  filePath?: string;
13
- lockFilePath?: string;
14
- writeLockFile?: boolean;
13
+ resolvedActions?: Record<string, string>;
15
14
  }
16
15
  export type RunnerDefinition = string | {
17
16
  group: string;
@@ -31,6 +30,6 @@ export declare class Workflow<JobStep extends Step = Step, Runner extends Runner
31
30
  setEnv(name: string, value: string): this;
32
31
  setConcurrencyGroup(concurrencyGroup: ConcurrencyGroup | null): this;
33
32
  defaultRunner(): string;
34
- compile(compileOptions?: CompileOptions): Promise<string | void>;
33
+ compile(compileOptions?: WorkflowCompileOptions): Promise<string | void>;
35
34
  }
36
35
  export {};
package/dist/workflow.js CHANGED
@@ -4,47 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Workflow = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
7
8
  const js_yaml_1 = require("js-yaml");
8
9
  const kebabCase_1 = __importDefault(require("lodash/kebabCase"));
9
- const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const util_1 = require("util");
12
- const axios_1 = __importDefault(require("axios"));
13
12
  const step_1 = require("./step");
14
13
  const writeFilePromise = (0, util_1.promisify)(fs_1.default.writeFile);
15
- const supplyChainAttack = async (step, compileOptions) => {
16
- const { lockFilePath, writeLockFile = false } = compileOptions;
14
+ const supplyChainAttack = async (step, resolvedActions) => {
17
15
  if (!(0, step_1.isUseStep)(step))
18
16
  return;
19
- // The user is not interested in frozen sha versions
20
- if (!lockFilePath)
21
- return step.uses;
22
17
  const uses = step.uses;
23
- const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
24
- // The uses is not a valid Github action with a tag
25
- if (!match)
26
- return uses;
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
- }
36
- const response = await axios_1.default.get(`https://api.github.com/repos/${repository}/tags`);
37
- const tags = response.data;
38
- const tag = tags.find((tag) => tag.name === version);
39
- if (!tag) {
40
- throw new Error(`Unable to retrieve ${uses} from Github tags`);
41
- }
42
- const result = `${repository}@${tag.commit.sha}`;
43
- chainAttackCache[uses] = result;
44
- if (writeLockFile) {
45
- fs_1.default.writeFileSync(lockFilePath, JSON.stringify(chainAttackCache, null, 2));
46
- }
47
- return result;
18
+ return resolvedActions[uses] ?? uses;
48
19
  };
49
20
  class Workflow {
50
21
  constructor(name) {
@@ -164,7 +135,7 @@ class Workflow {
164
135
  "working-directory": workingDirectory,
165
136
  "timeout-minutes": timeout,
166
137
  ...options,
167
- uses: await supplyChainAttack(step, compileOptions),
138
+ uses: await supplyChainAttack(step, compileOptions.resolvedActions ?? {}),
168
139
  };
169
140
  })),
170
141
  outputs,
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  const vitest_1 = require("vitest");
7
4
  const workflow_1 = require("./workflow");
8
- const fs_1 = __importDefault(require("fs"));
9
5
  (0, vitest_1.describe)("Workflow", () => {
10
6
  (0, vitest_1.it)("generates a simple workflow", async () => {
11
7
  const workflow = new workflow_1.Workflow("Simple");
@@ -282,31 +278,17 @@ exit 0`,
282
278
  });
283
279
  (0, vitest_1.expect)(await workflow.compile()).toMatchSnapshot();
284
280
  });
285
- (0, vitest_1.it)("supports supply chain attack, without writing the lock file", async () => {
281
+ (0, vitest_1.it)("supports supply chain attack", async () => {
286
282
  const workflow = new workflow_1.Workflow("Supply chain attack");
287
283
  workflow.on("push").addJob("job1", {
288
284
  steps: [
289
285
  { name: "Do something", uses: "tj-actions/changed-files@v45.0.7" },
290
286
  ],
291
287
  });
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
288
  (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,
289
+ resolvedActions: {
290
+ "tj-actions/changed-files@v45.0.7": "tj-actions/changed-files@youhavebeenhacked",
291
+ },
310
292
  })).toMatchSnapshot();
311
293
  });
312
294
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/gat",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Write your GitHub Actions workflows using TypeScript",
5
5
  "bin": {
6
6
  "gat": "dist/cli.js"
@@ -38,7 +38,7 @@
38
38
  "vitest": "^0.34.6"
39
39
  },
40
40
  "dependencies": {
41
- "axios": "^1.6.0",
41
+ "@octokit/rest": "^21.1.1",
42
42
  "js-yaml": "^4.1.0",
43
43
  "lodash": "^4.17.21"
44
44
  },