From fab420e41dd7ced21ef4ac3ddfdefb3f4e4fa62e Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 10 Nov 2020 20:40:20 +0000 Subject: [PATCH 1/5] Add checkIgnoredRepos helper --- checkIgnoredRepos.js | 16 ++++++++++++++++ checkIgnoredRepos.test.js | 30 ++++++++++++++++++++++++++++++ package-lock.json | 17 ++++++++++++++++- package.json | 1 + 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 checkIgnoredRepos.js create mode 100644 checkIgnoredRepos.test.js diff --git a/checkIgnoredRepos.js b/checkIgnoredRepos.js new file mode 100644 index 0000000..4a2970c --- /dev/null +++ b/checkIgnoredRepos.js @@ -0,0 +1,16 @@ +const matcher = require("matcher"); + +module.exports = function(input, ignored) { + // Nothing ignored, so it can't match + if (ignored.length === 0) { + return false; + } + + // Exact match + if (ignored.includes(input)) { + return true; + } + + // Glob match + return matcher([input], ignored).length > 0; +}; diff --git a/checkIgnoredRepos.test.js b/checkIgnoredRepos.test.js new file mode 100644 index 0000000..eb994de --- /dev/null +++ b/checkIgnoredRepos.test.js @@ -0,0 +1,30 @@ +const checkIgnoredRepos = require("./checkIgnoredRepos"); + +test("empty allow list", () => { + const actual = checkIgnoredRepos("mheap/demo", []); + expect(actual).toBe(false); +}); + +test("no match", () => { + expect(checkIgnoredRepos("mheap/demo", ["other/repo"])).toBe(false); +}); + +test("exact match", () => { + expect(checkIgnoredRepos("mheap/demo", ["mheap/demo"])).toBe(true); +}); + +test("partial match", () => { + expect(checkIgnoredRepos("mheap/demo", ["mheap/*"])).toBe(true); +}); + +test("no partial match", () => { + expect(checkIgnoredRepos("other/demo", ["mheap/*"])).toBe(false); +}); + +test("multiple ignores", () => { + expect(checkIgnoredRepos("mheap/demo", ["other", "mheap/*"])).toBe(true); +}); + +test("negative ignores", () => { + expect(checkIgnoredRepos("other/demo", ["!mheap/*"])).toBe(true); +}); diff --git a/package-lock.json b/package-lock.json index f3d2d9c..b61645f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3807,6 +3807,21 @@ "object-visit": "^1.0.0" } }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "requires": { + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, "merge-stream": { "version": "2.0.0", "resolved": "http://localhost:4873/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3857,7 +3872,7 @@ }, "minimatch": { "version": "3.0.4", - "resolved": "http://localhost:4873/minimatch/-/minimatch-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { diff --git a/package.json b/package.json index 567d00a..79f8d69 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@octokit/rest": "^16.35.2", "commander": "^4.0.1", + "matcher": "^3.0.0", "yaml": "^1.7.2" }, "bin": { From 10dce5f0d597ee3cdf4df61d001ead40907cc3f8 Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 10 Nov 2020 20:56:44 +0000 Subject: [PATCH 2/5] Accept the --ignore flag on the CLI --- bin.js | 12 ++++++++++-- index.js | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/bin.js b/bin.js index d6c5bb7..b149a0d 100755 --- a/bin.js +++ b/bin.js @@ -10,13 +10,21 @@ const packageDetails = require(path.join(__dirname, "package.json")); (async () => { try { // Allow for command line arguments - program.version(packageDetails.version).parse(process.argv); + program + .version(packageDetails.version) + .option( + "-i, --ignore ", + "comma separated list of actions to ignore e.g. mheap/debug-action. May be a glob e.g. mheap/*" + ) + .parse(process.argv); const filename = program.args[0]; + let ignored = program.opts().ignore; + ignored = (ignored || "").split(",").filter((r) => r); const input = fs.readFileSync(filename).toString(); - const output = await run(input); + const output = await run(input, ignored); fs.writeFileSync(filename, output.workflow); diff --git a/index.js b/index.js index c2abfe7..61ca79c 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,11 @@ const YAML = require("yaml"); const extractActions = require("./extractActions"); const replaceActions = require("./replaceActions"); const findRefOnGithub = require("./findRefOnGithub"); +const checkIgnoredRepos = require("./checkIgnoredRepos"); + +module.exports = async function(input, ignored) { + ignored = ignored || []; -module.exports = async function(input) { // Parse the workflow file let workflow = YAML.parseDocument(input); @@ -12,16 +15,22 @@ module.exports = async function(input) { let actions = extractActions(workflow); for (let i in actions) { + // Should this action be updated? + const action = `${actions[i].owner}/${actions[i].repo}`; + if (checkIgnoredRepos(action, ignored)) { + continue; + } + // Look up those actions on Github const newVersion = await findRefOnGithub(actions[i]); actions[i].newVersion = newVersion; // Rewrite each action, replacing the uses block with a specific sha - workflow = replaceActions(workflow, actions[i]); + workflow = replaceActions(workflow, actions[i], ignored); } return { workflow: workflow.toString(), - actions + actions, }; }; From eb0ff1388685a1b08b4fe2f91c21d6fddb5bfa9f Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 10 Nov 2020 21:09:30 +0000 Subject: [PATCH 3/5] Add README for ignoring actions --- README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4762d1f..09dbee8 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ jobs: name: nexmo/github-actions/submodule-auto-pr@master runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: nexmo/github-actions/submodule-auto-pr - uses: nexmo/github-actions/submodule-auto-pr@master + - uses: actions/checkout@master + - name: nexmo/github-actions/submodule-auto-pr + uses: nexmo/github-actions/submodule-auto-pr@master ``` In to this: @@ -68,14 +68,63 @@ GH_ADMIN_TOKEN= pin-github-action /path/to/.github/workflows/yo Run it as many times as you like! Each time you run the tool the exact sha will be updated to the latest available sha for your pinned ref. +## Leaving Actions unpinned + +To leave an action unpinned, pass the `--ignore` option when running `pin-github-action`. + +Running `pin-github-action /path/to/.github/workflows/your-name.yml --ignore "actions/*"` will turn this: + +```yaml +jobs: + build: + name: nexmo/github-actions/submodule-auto-pr@master + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: nexmo/github-actions/submodule-auto-pr@master +``` + +Into this (notice how `actions/checkout@master` is ignored): + +```yaml +jobs: + build: + name: nexmo/github-actions/submodule-auto-pr@master + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: nexmo/github-actions/submodule-auto-pr + uses: nexmo/github-actions/submodule-auto-pr@73549280c1c566830040d9a01fe9050dae6a3036 # pin@master +``` + +You can pass multiple actions to ignore as a comma separated list e.g. `actions/checkout,mheap/*` + +A quick overview of the available globbing patterns (taken from [multimatch](https://github.com/sindresorhus/multimatch), which we use to match globs): + +- `*` matches any number of characters, but not `/` +- `?` matches a single character, but not `/` +- `**` matches any number of characters, including `/`, as long as it's the only thing in a path part +- `{}` allows for a comma-separated list of "or" expressions +- `!` at the beginning of a pattern will negate the match + +Examples: + +- Exact match: `actions/checkout` +- Partial match: `actions/*` +- Negated match: `!actions/*` (will only pin `actions/*` actions) + ## How it works -* Load the workflow file provided -* Tokenise it in to an AST -* Extract all `uses` steps, skipping any `docker://` or `./local-path` actions -* Loop through all `uses` steps to determine the target ref - * If there's a comment in the step, remove `pin@` and use that as the target - * Otherwise, fall back to the ref in the action as the default -* Look up the current sha for each repo on GitHub and update the action to use the specific hash - * If needed, add a comment with the target pinned version -* Write the workflow file with the new pinned version and original target version as a comment +- Load the workflow file provided +- Tokenise it in to an AST +- Extract all `uses` steps, skipping any `docker://` or `./local-path` actions +- Loop through all `uses` steps to determine the target ref + - If there's a comment in the step, remove `pin@` and use that as the target + - Otherwise, fall back to the ref in the action as the default +- Look up the current sha for each repo on GitHub and update the action to use the specific hash + - If needed, add a comment with the target pinned version +- Write the workflow file with the new pinned version and original target version as a comment + +``` + +``` From 633a3157051775274ef9c06cfed4b027bc1a52d9 Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 10 Nov 2020 21:14:34 +0000 Subject: [PATCH 4/5] Rename ignored to allowed --- README.md | 6 +++--- bin.js | 10 +++++----- checkIgnoredRepos.js => checkAllowedRepos.js | 0 checkIgnoredRepos.test.js | 16 ++++++++-------- index.js | 10 +++++----- 5 files changed, 21 insertions(+), 21 deletions(-) rename checkIgnoredRepos.js => checkAllowedRepos.js (100%) diff --git a/README.md b/README.md index 09dbee8..55d18e2 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,9 @@ be updated to the latest available sha for your pinned ref. ## Leaving Actions unpinned -To leave an action unpinned, pass the `--ignore` option when running `pin-github-action`. +To leave an action unpinned, pass the `--allow` option when running `pin-github-action`. -Running `pin-github-action /path/to/.github/workflows/your-name.yml --ignore "actions/*"` will turn this: +Running `pin-github-action /path/to/.github/workflows/your-name.yml --allow "actions/*"` will turn this: ```yaml jobs: @@ -97,7 +97,7 @@ jobs: uses: nexmo/github-actions/submodule-auto-pr@73549280c1c566830040d9a01fe9050dae6a3036 # pin@master ``` -You can pass multiple actions to ignore as a comma separated list e.g. `actions/checkout,mheap/*` +You can pass multiple actions to allow as a comma separated list e.g. `actions/checkout,mheap/*` A quick overview of the available globbing patterns (taken from [multimatch](https://github.com/sindresorhus/multimatch), which we use to match globs): diff --git a/bin.js b/bin.js index b149a0d..b4ef2d4 100755 --- a/bin.js +++ b/bin.js @@ -13,18 +13,18 @@ const packageDetails = require(path.join(__dirname, "package.json")); program .version(packageDetails.version) .option( - "-i, --ignore ", - "comma separated list of actions to ignore e.g. mheap/debug-action. May be a glob e.g. mheap/*" + "-a, --allow ", + "comma separated list of actions to allow e.g. mheap/debug-action. May be a glob e.g. mheap/*" ) .parse(process.argv); const filename = program.args[0]; - let ignored = program.opts().ignore; - ignored = (ignored || "").split(",").filter((r) => r); + let allowed = program.opts().allow; + allowed = (allowed || "").split(",").filter((r) => r); const input = fs.readFileSync(filename).toString(); - const output = await run(input, ignored); + const output = await run(input, allowed); fs.writeFileSync(filename, output.workflow); diff --git a/checkIgnoredRepos.js b/checkAllowedRepos.js similarity index 100% rename from checkIgnoredRepos.js rename to checkAllowedRepos.js diff --git a/checkIgnoredRepos.test.js b/checkIgnoredRepos.test.js index eb994de..a2e467d 100644 --- a/checkIgnoredRepos.test.js +++ b/checkIgnoredRepos.test.js @@ -1,30 +1,30 @@ -const checkIgnoredRepos = require("./checkIgnoredRepos"); +const checkAllowedRepos = require("./checkAllowedRepos"); test("empty allow list", () => { - const actual = checkIgnoredRepos("mheap/demo", []); + const actual = checkAllowedRepos("mheap/demo", []); expect(actual).toBe(false); }); test("no match", () => { - expect(checkIgnoredRepos("mheap/demo", ["other/repo"])).toBe(false); + expect(checkAllowedRepos("mheap/demo", ["other/repo"])).toBe(false); }); test("exact match", () => { - expect(checkIgnoredRepos("mheap/demo", ["mheap/demo"])).toBe(true); + expect(checkAllowedRepos("mheap/demo", ["mheap/demo"])).toBe(true); }); test("partial match", () => { - expect(checkIgnoredRepos("mheap/demo", ["mheap/*"])).toBe(true); + expect(checkAllowedRepos("mheap/demo", ["mheap/*"])).toBe(true); }); test("no partial match", () => { - expect(checkIgnoredRepos("other/demo", ["mheap/*"])).toBe(false); + expect(checkAllowedRepos("other/demo", ["mheap/*"])).toBe(false); }); test("multiple ignores", () => { - expect(checkIgnoredRepos("mheap/demo", ["other", "mheap/*"])).toBe(true); + expect(checkAllowedRepos("mheap/demo", ["other", "mheap/*"])).toBe(true); }); test("negative ignores", () => { - expect(checkIgnoredRepos("other/demo", ["!mheap/*"])).toBe(true); + expect(checkAllowedRepos("other/demo", ["!mheap/*"])).toBe(true); }); diff --git a/index.js b/index.js index 61ca79c..f8bb0af 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,10 @@ const YAML = require("yaml"); const extractActions = require("./extractActions"); const replaceActions = require("./replaceActions"); const findRefOnGithub = require("./findRefOnGithub"); -const checkIgnoredRepos = require("./checkIgnoredRepos"); +const checkAllowedRepos = require("./checkAllowedRepos"); -module.exports = async function(input, ignored) { - ignored = ignored || []; +module.exports = async function(input, allowed) { + allowed = allowed || []; // Parse the workflow file let workflow = YAML.parseDocument(input); @@ -17,7 +17,7 @@ module.exports = async function(input, ignored) { for (let i in actions) { // Should this action be updated? const action = `${actions[i].owner}/${actions[i].repo}`; - if (checkIgnoredRepos(action, ignored)) { + if (checkAllowedRepos(action, allowed)) { continue; } @@ -26,7 +26,7 @@ module.exports = async function(input, ignored) { actions[i].newVersion = newVersion; // Rewrite each action, replacing the uses block with a specific sha - workflow = replaceActions(workflow, actions[i], ignored); + workflow = replaceActions(workflow, actions[i]); } return { From 29cb233fcddadfb9eb55b674b203e628b7150135 Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 10 Nov 2020 21:22:34 +0000 Subject: [PATCH 5/5] Run lint-fix --- bin.js | 2 +- index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin.js b/bin.js index b4ef2d4..02d6b07 100755 --- a/bin.js +++ b/bin.js @@ -20,7 +20,7 @@ const packageDetails = require(path.join(__dirname, "package.json")); const filename = program.args[0]; let allowed = program.opts().allow; - allowed = (allowed || "").split(",").filter((r) => r); + allowed = (allowed || "").split(",").filter(r => r); const input = fs.readFileSync(filename).toString(); diff --git a/index.js b/index.js index f8bb0af..ae01aef 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,6 @@ module.exports = async function(input, allowed) { return { workflow: workflow.toString(), - actions, + actions }; };