diff --git a/extractActions.js b/extractActions.js index 7d5f7c4..a60b5db 100644 --- a/extractActions.js +++ b/extractActions.js @@ -23,7 +23,7 @@ function extractFromComposite(input, allowEmpty) { const actions = new Set(); steps = steps[0].value.items; for (let step of steps) { - handleStep(actions, step); + handleStep(actions, step.items); } return Array.from(actions); } @@ -41,18 +41,26 @@ function extractFromWorkflow(input, allowEmpty) { } for (let job of jobs) { - let steps = job.value.items.filter((n) => n.key == "steps"); + // Check for + let steps = job.value.items.filter( + (n) => n.key == "steps" || n.key == "uses" + ); if (!steps.length) { - throw new Error("No job.steps found"); + throw new Error("No job.steps or job.uses found"); } - steps = steps[0].value.items; - if (!steps.length) { - throw new Error("No job.steps found"); - } - - for (let step of steps) { - handleStep(actions, step); + // It's a job with steps + if (steps[0].value.items) { + if (!steps[0].value.items.length) { + throw new Error("No job.steps found"); + } + + for (let step of steps[0].value.items) { + handleStep(actions, step.items); + } + } else { + // It's a job that calls a reusable workflow + handleStep(actions, steps); } } @@ -63,8 +71,8 @@ function extractFromWorkflow(input, allowEmpty) { return Array.from(actions); } -function handleStep(actions, step) { - const uses = step.items.filter((n) => n.key == "uses"); +function handleStep(actions, items) { + const uses = items.filter((n) => n.key == "uses"); for (let use of uses) { const line = use.value.value.toString(); diff --git a/extractActions.test.js b/extractActions.test.js index 07e19f7..b84c60f 100644 --- a/extractActions.test.js +++ b/extractActions.test.js @@ -245,7 +245,7 @@ test("throws with missing steps", () => { }); const actual = () => extractActions(input); - expect(actual).toThrow("No job.steps found"); + expect(actual).toThrow("No job.steps or job.uses found"); }); test("throws with empty steps", () => { @@ -367,6 +367,29 @@ test("extracts from composite actions", () => { ]); }); +test("extracts from reusable workflows", () => { + const input = convertToAst({ + name: "Sample Reusable", + jobs: { + test: { + uses: "mheap/test-action@master", + } + }, + }); + + const actual = extractActions(input); + + expect(actual).toEqual([ + { + owner: "mheap", + repo: "test-action", + path: "", + currentVersion: "master", + pinnedVersion: "master", + }, + ]); +}); + function convertToAst(input) { return YAML.parseDocument(YAML.stringify(input)); } diff --git a/replaceActions.js b/replaceActions.js index 5886439..116c10b 100644 --- a/replaceActions.js +++ b/replaceActions.js @@ -3,10 +3,11 @@ module.exports = function (input, action) { if (runs.length) { return replaceInComposite(input, action); } - return replaceInWorkflow(input, action); }; +function replaceInReusable(input, action) {} + function replaceInComposite(input, action) { let actionString = `${action.owner}/${action.repo}`; if (action.path) { @@ -48,18 +49,41 @@ function replaceInWorkflow(input, action) { .items; for (let job of jobs) { - const steps = job.value.items.filter((n) => n.key == "steps")[0].value - .items; - for (let step of steps) { - const uses = step.items.filter((n) => n.key == "uses"); - for (let use of uses) { - if (use.value.value == actionString) { - use.value.value = replacement; - use.value.comment = ` pin@${action.pinnedVersion}`; - } - } + const stepKeys = job.value.items.filter((n) => n.key == "steps"); + const usesKeys = job.value.items.filter((n) => n.key == "uses"); + + if (stepKeys.length) { + replaceStandard( + stepKeys[0].value.items, + actionString, + replacement, + action + ); + } else if (usesKeys.length) { + replaceReusable(usesKeys, actionString, replacement, action); } } return input; } + +function replaceReusable(uses, actionString, replacement, action) { + for (let use of uses) { + if (use.value.value == actionString) { + use.value.value = replacement; + use.value.comment = ` pin@${action.pinnedVersion}`; + } + } +} + +function replaceStandard(steps, actionString, replacement, action) { + for (let step of steps) { + const uses = step.items.filter((n) => n.key == "uses"); + for (let use of uses) { + if (use.value.value == actionString) { + use.value.value = replacement; + use.value.comment = ` pin@${action.pinnedVersion}`; + } + } + } +} diff --git a/replaceActions.test.js b/replaceActions.test.js index 403838e..c62c03f 100644 --- a/replaceActions.test.js +++ b/replaceActions.test.js @@ -51,6 +51,21 @@ test("replaces a single action with a sha (composite)", () => { expect(actual).toContain("uses: mheap/test-action@sha-here # pin@master"); }); +test("replaces a single action with a sha (reusable)", () => { + const input = convertToAst({ + name: "Sample Reusable", + jobs: { + test: { + uses: "mheap/test-action@master", + } + }, + }); + + const actual = replaceActions(input, { ...action }).toString(); + + expect(actual).toContain("uses: mheap/test-action@sha-here # pin@master"); +}); + test("replaces an existing sha with a different sha, not changing the pinned branch", () => { const input = convertToAst({ name: "PR",