@factorialco/gat 3.3.1 → 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,60 +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 rest_1 = require("@octokit/rest");
13
12
  const step_1 = require("./step");
14
13
  const writeFilePromise = (0, util_1.promisify)(fs_1.default.writeFile);
15
- let firstCompileCall = true;
16
- const supplyChainAttack = async (step, compileOptions) => {
17
- const { lockFilePath, writeLockFile = false } = compileOptions;
14
+ const supplyChainAttack = async (step, resolvedActions) => {
18
15
  if (!(0, step_1.isUseStep)(step))
19
16
  return;
20
- // The user is not interested in frozen sha versions
21
- if (!lockFilePath)
22
- return step.uses;
23
17
  const uses = step.uses;
24
- const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
25
- // The uses is not a valid Github action with a tag
26
- if (!match)
27
- return uses;
28
- const { repository, version } = match.groups;
29
- if (firstCompileCall && writeLockFile && fs_1.default.existsSync(lockFilePath)) {
30
- firstCompileCall = false;
31
- fs_1.default.rmSync(lockFilePath);
32
- }
33
- const chainAttackCache = fs_1.default.existsSync(lockFilePath)
34
- ? JSON.parse(fs_1.default.readFileSync(lockFilePath, "utf8"))
35
- : {};
36
- if (chainAttackCache[uses])
37
- return chainAttackCache[uses];
38
- if (!writeLockFile) {
39
- throw new Error(`Unable to retrieve ${uses} from lock file and writeLockFile is false`);
40
- }
41
- const [owner, repo] = repository.split("/");
42
- const octokit = process.env.GITHUB_TOKEN
43
- ? new rest_1.Octokit({
44
- auth: process.env.GITHUB_TOKEN,
45
- })
46
- : new rest_1.Octokit();
47
- const response = await octokit.rest.repos.listTags({
48
- owner,
49
- repo,
50
- });
51
- const tag = response.data.find((tag) => tag.name === version);
52
- if (!tag) {
53
- throw new Error(`Unable to retrieve ${uses} from Github tags`);
54
- }
55
- const result = `${repository}@${tag.commit.sha}`;
56
- chainAttackCache[uses] = result;
57
- if (writeLockFile) {
58
- fs_1.default.writeFileSync(lockFilePath, JSON.stringify(chainAttackCache, null, 2));
59
- }
60
- return result;
18
+ return resolvedActions[uses] ?? uses;
61
19
  };
62
20
  class Workflow {
63
21
  constructor(name) {
@@ -177,7 +135,7 @@ class Workflow {
177
135
  "working-directory": workingDirectory,
178
136
  "timeout-minutes": timeout,
179
137
  ...options,
180
- uses: await supplyChainAttack(step, compileOptions),
138
+ uses: await supplyChainAttack(step, compileOptions.resolvedActions ?? {}),
181
139
  };
182
140
  })),
183
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.1",
3
+ "version": "3.4.0",
4
4
  "description": "Write your GitHub Actions workflows using TypeScript",
5
5
  "bin": {
6
6
  "gat": "dist/cli.js"