Repository: mheap/pin-github-action
Pull request #182 (2025-01-27 15:09:07)

Take folder name as arg
Allow passing folder name to process all files inside folder at once. Contains also https://github.com/mheap/pin-github-action/pull/181

mheap: Thanks for the contribution @gladykov! Could you please add some tests for this functionality?
gladykov: @mheap Moved code to a separate method and added test

Patch:
diff --git a/README.md b/README.md
index 6f56ae1..73148f0 100644
--- a/README.md
+++ b/README.md
@@ -62,10 +62,18 @@ alias pin-github-action="docker run --rm -v $(pwd):/src -e GH_ADMIN_TOKEN mheap/
 
 ## Usage
 
+Use on single file:
+
 ```bash
 pin-github-action /path/to/.github/workflows/your-name.yml
 ```
 
+Use on all YAML|YML files in directory:
+
+```bash
+pin-github-action /path/to/.github/workflows/
+```
+
 If you use private actions (or are hitting rate limits), you'll need to provide
 a GitHub access token:
 
@@ -82,12 +90,18 @@ If you're having issues, run with debug logging enabled and open an issue:
 DEBUG="pin-github-action*" pin-github-action /path/to/.github/workflows/your-name.yml
 ```
 
-You can process multiple files at once by adding additional files as arguments:
+You can pass multiple files at once by adding additional files as arguments:
 
 ```bash
 pin-github-action first.yml second.yml
 ```
 
+And you can mix directories with files:
+
+```bash
+pin-github-action first.yml /path/to/.github/workflows/
+```
+
 ## Leaving Actions unpinned
 
 To leave an action unpinned, pass the `--allow` option when running `pin-github-action`.
diff --git a/bin.js b/bin.js
index fe305a7..9a52e71 100755
--- a/bin.js
+++ b/bin.js
@@ -6,7 +6,9 @@ import debugLib from "debug";
 const debug = debugLib("pin-github-action");
 
 import run from "./index.js";
+import collectWorkflowFiles from "./collectWorkflowFiles.js";
 
+const mainDebug = debug.extend("main-program");
 const packageDetails = JSON.parse(
   fs.readFileSync(new URL("./package.json", import.meta.url))
 );
@@ -17,7 +19,7 @@ const packageDetails = JSON.parse(
     program
       .name("pin-github-action")
       .version(packageDetails.version)
-      .usage("[options] [file ...]")
+      .usage("[options] [file||directory ...]")
       .option(
         "-a, --allow <actions>",
         "comma separated list of actions to allow e.g. mheap/debug-action. May be a glob e.g. mheap/*"
@@ -37,7 +39,7 @@ const packageDetails = JSON.parse(
       )
       .parse(process.argv);
 
-    if (program.args.length == 0) {
+    if (program.args.length === 0) {
       program.help();
     }
 
@@ -47,13 +49,20 @@ const packageDetails = JSON.parse(
     let allowEmpty = program.opts().allowEmpty;
     let comment = program.opts().comment;
 
-    for (const filename of program.args) {
-      if (!fs.existsSync(filename)) {
-        throw "No such file: " + filename;
+    for (const pathname of program.args) {
+      if (!fs.existsSync(pathname)) {
+        throw "No such file or directory: " + pathname;
       }
     }
 
-    for (const filename of program.args) {
+    const filesToProcess = collectWorkflowFiles(program.args);
+
+    if (filesToProcess.length === 0) {
+      throw "Didn't find Y(A)ML files in provided paths: " + program.args;
+    }
+
+    for (const filename of filesToProcess) {
+      mainDebug("Processing " + filename);
       const input = fs.readFileSync(filename).toString();
       const output = await run(
         input,
diff --git a/collectWorkflowFiles.js b/collectWorkflowFiles.js
new file mode 100644
index 0000000..ce39ee7
--- /dev/null
+++ b/collectWorkflowFiles.js
@@ -0,0 +1,23 @@
+import fs from "fs";
+import path from "path";
+
+export default function (programArgs) {
+  let filesToProcess = [];
+
+  for (const pathname of programArgs) {
+    if (fs.lstatSync(pathname).isFile()) {
+      filesToProcess.push(pathname);
+    } else {
+      const files = fs
+        .readdirSync(pathname)
+        .filter((f) => {
+          return f.includes(".yaml") || f.includes(".yml");
+        })
+        .map((f) => path.join(pathname, f));
+      filesToProcess = filesToProcess.concat(files);
+    }
+  }
+
+  // If user will pass both file and directory, make sure to clear duplicates
+  return [...new Set(filesToProcess)];
+}


Test patch:
diff --git a/collectWorkflowFiles.test.js b/collectWorkflowFiles.test.js
new file mode 100644
index 0000000..3e7aa55
--- /dev/null
+++ b/collectWorkflowFiles.test.js
@@ -0,0 +1,29 @@
+import collectWorkflowFiles from "./collectWorkflowFiles.js";
+
+test("collects all files from given path without duplicates", () => {
+  const programArgs = ["__fixtures__/composite.yml", "__fixtures__/"];
+
+  expect(collectWorkflowFiles(programArgs)).toEqual([
+    "__fixtures__/composite.yml",
+    "__fixtures__/reusable.yml",
+    "__fixtures__/workflow.yml",
+  ]);
+});
+
+test("collects all files from given path", () => {
+  const programArgs = ["__fixtures__/"];
+
+  expect(collectWorkflowFiles(programArgs)).toEqual([
+    "__fixtures__/composite.yml",
+    "__fixtures__/reusable.yml",
+    "__fixtures__/workflow.yml",
+  ]);
+});
+
+test("collects single file from given path", () => {
+  const programArgs = ["__fixtures__/composite.yml"];
+
+  expect(collectWorkflowFiles(programArgs)).toEqual([
+    "__fixtures__/composite.yml",
+  ]);
+});