Skip to content

Commit 334b6ef

Browse files
beagleknightfcsonline
authored andcommitted
Refactor how supply chain generation
1 parent ee5f0cb commit 334b6ef

File tree

4 files changed

+101
-118
lines changed

4 files changed

+101
-118
lines changed

src/__snapshots__/workflow.spec.ts.snap

+1-18
Original file line numberDiff line numberDiff line change
@@ -377,24 +377,7 @@ jobs:
377377
"
378378
`;
379379

380-
exports[`Workflow > supports supply chain attack, with writing the lock file 1`] = `
381-
"# Workflow automatically generated by gat
382-
# DO NOT CHANGE THIS FILE MANUALLY
383-
384-
name: Supply chain attack
385-
on:
386-
push: null
387-
jobs:
388-
job1:
389-
runs-on: ubuntu-22.04
390-
timeout-minutes: 15
391-
steps:
392-
- name: Do something
393-
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
394-
"
395-
`;
396-
397-
exports[`Workflow > supports supply chain attack, without writing the lock file 1`] = `
380+
exports[`Workflow > supports supply chain attack 1`] = `
398381
"# Workflow automatically generated by gat
399382
# DO NOT CHANGE THIS FILE MANUALLY
400383

src/template.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import fs from "fs";
2+
import { Octokit } from "@octokit/rest";
3+
4+
import { Workflow } from "./workflow";
5+
6+
interface CompileOptions {
7+
templates: Record<string, Workflow>;
8+
lockFilePath: string;
9+
writeLockFile: boolean;
10+
}
11+
12+
const createLockFile = async (
13+
templates: Record<string, Workflow>,
14+
lockFilePath: string,
15+
) => {
16+
const actions = Object.values(templates).reduce<string[]>((acc, template) => {
17+
template.jobs.forEach((job) => {
18+
if ("steps" in job.options) {
19+
job.options.steps.forEach((step) => {
20+
if ("uses" in step) {
21+
acc.push(step.uses);
22+
}
23+
});
24+
}
25+
});
26+
27+
return acc;
28+
}, []);
29+
30+
const uniqueActions = [...new Set(actions)];
31+
32+
const octokit = process.env.GITHUB_TOKEN
33+
? new Octokit({
34+
auth: process.env.GITHUB_TOKEN,
35+
})
36+
: new Octokit();
37+
38+
const resolvedActions: Record<string, string> = {};
39+
40+
await Promise.all(
41+
uniqueActions.map(async (action) => {
42+
const match = action.match(/(?<repository>.*)@(?<version>.*)/);
43+
44+
if (!match) return;
45+
46+
const { repository, version } = match.groups as {
47+
repository: string;
48+
version: string;
49+
};
50+
51+
const [owner, repo] = repository.split("/");
52+
const response = await octokit.rest.repos.listTags({
53+
owner,
54+
repo,
55+
});
56+
57+
const tag = response.data.find((tag) => tag.name === version);
58+
59+
if (!tag) {
60+
throw new Error(`Unable to retrieve ${action} from Github tags`);
61+
}
62+
63+
resolvedActions[action] = `${repository}@${tag.commit.sha}`;
64+
}),
65+
);
66+
67+
fs.writeFileSync(lockFilePath, JSON.stringify(resolvedActions, null, 2));
68+
};
69+
70+
export const compileTemplates = async (options: CompileOptions) => {
71+
const { templates, lockFilePath, writeLockFile } = options;
72+
73+
if (writeLockFile) {
74+
await createLockFile(templates, lockFilePath);
75+
}
76+
77+
const resolvedActions = fs.existsSync(lockFilePath)
78+
? JSON.parse(fs.readFileSync(lockFilePath, "utf8"))
79+
: {};
80+
81+
return Promise.all(
82+
Object.entries(templates).map(([filePath, template]) =>
83+
template.compile({ filePath, resolvedActions }),
84+
),
85+
);
86+
};

src/workflow.spec.ts

+5-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, expect, it } from "vitest";
22
import { RunStep, UseStep } from "./step";
33
import { Workflow } from "./workflow";
4-
import fs from "fs";
54

65
describe("Workflow", () => {
76
it("generates a simple workflow", async () => {
@@ -310,7 +309,7 @@ exit 0`,
310309
expect(await workflow.compile()).toMatchSnapshot();
311310
});
312311

313-
it("supports supply chain attack, without writing the lock file", async () => {
312+
it("supports supply chain attack", async () => {
314313
const workflow = new Workflow("Supply chain attack");
315314

316315
workflow.on("push").addJob("job1", {
@@ -319,39 +318,12 @@ exit 0`,
319318
],
320319
});
321320

322-
fs.writeFileSync(
323-
"/tmp/lockfile1.json",
324-
JSON.stringify(
325-
{
321+
expect(
322+
await workflow.compile({
323+
resolvedActions: {
326324
"tj-actions/changed-files@v45.0.7":
327325
"tj-actions/changed-files@youhavebeenhacked",
328326
},
329-
null,
330-
2,
331-
),
332-
);
333-
334-
expect(
335-
await workflow.compile({
336-
lockFilePath: "/tmp/lockfile1.json",
337-
writeLockFile: false,
338-
}),
339-
).toMatchSnapshot();
340-
});
341-
342-
it("supports supply chain attack, with writing the lock file", async () => {
343-
const workflow = new Workflow("Supply chain attack");
344-
345-
workflow.on("push").addJob("job1", {
346-
steps: [
347-
{ name: "Do something", uses: "tj-actions/changed-files@v45.0.7" },
348-
],
349-
});
350-
351-
expect(
352-
await workflow.compile({
353-
lockFilePath: "/tmp/lockfile2.json",
354-
writeLockFile: true,
355327
}),
356328
).toMatchSnapshot();
357329
});

src/workflow.ts

+9-67
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ interface EnvVar {
2626
value: string;
2727
}
2828

29-
interface CompileOptions {
29+
interface WorkflowCompileOptions {
3030
filePath?: string;
31-
lockFilePath?: string;
32-
writeLockFile?: boolean;
31+
resolvedActions?: Record<string, string>;
3332
}
3433

3534
export type RunnerDefinition =
@@ -41,73 +40,13 @@ let firstCompileCall = true;
4140

4241
const supplyChainAttack = async (
4342
step: Step,
44-
compileOptions: CompileOptions,
43+
resolvedActions: Record<string, string>,
4544
) => {
46-
const { lockFilePath, writeLockFile = false } = compileOptions;
47-
4845
if (!isUseStep(step)) return;
4946

50-
// The user is not interested in frozen sha versions
51-
if (!lockFilePath) return step.uses;
52-
5347
const uses = step.uses;
5448

55-
const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
56-
57-
// The uses is not a valid Github action with a tag
58-
if (!match) return uses;
59-
60-
const { repository, version } = match.groups as {
61-
repository: string;
62-
version: string;
63-
};
64-
65-
if (firstCompileCall && writeLockFile && fs.existsSync(lockFilePath)) {
66-
firstCompileCall = false;
67-
68-
fs.rmSync(lockFilePath);
69-
}
70-
71-
const chainAttackCache = fs.existsSync(lockFilePath)
72-
? JSON.parse(fs.readFileSync(lockFilePath, "utf8"))
73-
: {};
74-
75-
if (chainAttackCache[uses]) return chainAttackCache[uses];
76-
77-
if (!writeLockFile) {
78-
throw new Error(
79-
`Unable to retrieve ${uses} from lock file and writeLockFile is false`,
80-
);
81-
}
82-
83-
const [owner, repo] = repository.split("/");
84-
85-
const octokit = process.env.GITHUB_TOKEN
86-
? new Octokit({
87-
auth: process.env.GITHUB_TOKEN,
88-
})
89-
: new Octokit();
90-
91-
const response = await octokit.rest.repos.listTags({
92-
owner,
93-
repo,
94-
});
95-
96-
const tag = response.data.find((tag) => tag.name === version);
97-
98-
if (!tag) {
99-
throw new Error(`Unable to retrieve ${uses} from Github tags`);
100-
}
101-
102-
const result = `${repository}@${tag.commit.sha}`;
103-
104-
chainAttackCache[uses] = result;
105-
106-
if (writeLockFile) {
107-
fs.writeFileSync(lockFilePath, JSON.stringify(chainAttackCache, null, 2));
108-
}
109-
110-
return result;
49+
return resolvedActions[uses] ?? uses;
11150
};
11251

11352
export class Workflow<
@@ -162,7 +101,7 @@ export class Workflow<
162101
return "ubuntu-22.04";
163102
}
164103

165-
async compile(compileOptions: CompileOptions = {}) {
104+
async compile(compileOptions: WorkflowCompileOptions = {}) {
166105
const result = {
167106
name: this.name,
168107
on: Object.fromEntries(
@@ -284,7 +223,10 @@ export class Workflow<
284223
"working-directory": workingDirectory,
285224
"timeout-minutes": timeout,
286225
...options,
287-
uses: await supplyChainAttack(step, compileOptions),
226+
uses: await supplyChainAttack(
227+
step,
228+
compileOptions.resolvedActions ?? {},
229+
),
288230
};
289231
}),
290232
),

0 commit comments

Comments
 (0)