Repository: mheap/pin-github-action
Pull request #193 (2025-03-16 18:14:02)

[feature] Don't fail fast on list of actions
When running this on a big list of files, it's kind of annoying if it fails when it encounters a single action it can't parse properly.


Patch:
diff --git a/README.md b/README.md
index 4ada0be..f71d4be 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ npm install -g pin-github-action
 ### Docker
 
 ```bash
-alias pin-github-action="docker run --rm -v $(pwd):/workflows -e GITHUB_TOKEN mheap/pin-github-action"
+alias pin-github-action='docker run --rm -v $(pwd):/workflows -e GITHUB_TOKEN mheap/pin-github-action'
 ```
 
 ## Usage
@@ -74,6 +74,12 @@ Use on all YAML|YML files in directory:
 pin-github-action /path/to/.github/workflows/
 ```
 
+If you run the tool on a directory and want to continue processing when a single file fails, pass the `--continue-on-error` parameter:
+
+```bash
+pin-github-action --continue-on-error /path/to/.github/workflows/
+```
+
 If you use private actions (or are hitting rate limits), you'll need to provide
 a GitHub access token:
 
@@ -178,3 +184,19 @@ pin-github-action -c " pin@{ref}" /path/to/workflow.yaml
 - 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
+
+## Contributing
+
+1. **Run tests**: Ensure all tests pass before submitting your changes.
+
+```bash
+npm install
+npm test
+```
+
+2. **Build and test locally using Docker**: You can build a container and test the application locally.
+
+```bash
+docker build -t pin-github-action .
+docker run --rm -v $(pwd):/workflows -e GITHUB_TOKEN=<your-token-here> pin-github-action /path/to/.github/workflows/your-name.yml
+```
diff --git a/bin.js b/bin.js
index e60c3e8..111c535 100755
--- a/bin.js
+++ b/bin.js
@@ -39,6 +39,10 @@ const packageDetails = JSON.parse(
         "comment to add inline when pinning an action",
         " {ref}",
       )
+      .option(
+        "--continue-on-error",
+        "continue processing files even if an error occurs",
+      )
       .option(
         "--enforce <filename>",
         "create a workflow at <filename> that ensures all actions are pinned",
@@ -54,6 +58,7 @@ const packageDetails = JSON.parse(
     let ignoreShas = program.opts().ignoreShas;
     let allowEmpty = program.opts().allowEmpty;
     let comment = program.opts().comment;
+    const continueOnError = program.opts().continueOnError;
 
     // Check if we're adding the security file
     if (program.opts().enforce) {
@@ -77,14 +82,27 @@ const packageDetails = JSON.parse(
     for (const filename of filesToProcess) {
       mainDebug("Processing " + filename);
       const input = fs.readFileSync(filename).toString();
-      const output = await run(
-        input,
-        allowed,
-        ignoreShas,
-        allowEmpty,
-        debug,
-        comment,
-      );
+      let output;
+      try {
+        output = await run(
+          input,
+          allowed,
+          ignoreShas,
+          allowEmpty,
+          debug,
+          comment,
+        );
+      }
+      catch (e) {
+        if (!continueOnError) {
+          throw e;
+        }
+        else {
+          console.error(`Failed to process ${filename} due to: ${e.message || e}`);
+          console.error(`Continuing with next file because multiple files are being processed...`);
+          continue;
+        }
+      }
       fs.writeFileSync(filename, output.input);
     }
 
diff --git a/extractActions.js b/extractActions.js
index 989cad4..9550b81 100644
--- a/extractActions.js
+++ b/extractActions.js
@@ -41,7 +41,13 @@ function extractFromWorkflow(input, allowEmpty, comment) {
   }
 
   for (let job of jobs) {
-    // Check for
+    if (!job.value) {
+      throw new Error(
+        "Error parsing workflow. Is this a valid GitHub Actions workflow?\n\n" +
+          input.toString(),
+      );
+    }
+
     let steps = job.value.items.filter(
       (n) => n.key == "steps" || n.key == "uses",
     );
diff --git a/findRefOnGithub.js b/findRefOnGithub.js
index 9b588f9..2808371 100644
--- a/findRefOnGithub.js
+++ b/findRefOnGithub.js
@@ -101,8 +101,14 @@ function handleCommonErrors(e, name) {
   }
 
   if (e.message.includes("API rate limit exceeded")) {
-    debug(`[${name}] ERROR: Rate Limiting error`);
-    return e.message;
+    const resetTime = e.response?.headers["x-ratelimit-reset"];
+    const resetDate = resetTime
+      ? new Date(resetTime * 1000).toLocaleString()
+      : "unknown";
+    debug(
+      `[${name}] ERROR: Rate Limiting error. Limit resets at: ${resetDate}`,
+    );
+    return `${e.message} Limit resets at: ${resetDate}`;
   }
   return "";
 }
diff --git a/package-lock.json b/package-lock.json
index f661311..af03f97 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "pin-github-action",
-  "version": "3.1.1",
+  "version": "3.1.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "pin-github-action",
-      "version": "3.1.1",
+      "version": "3.1.2",
       "license": "MIT",
       "dependencies": {
         "@octokit/rest": "^21",
diff --git a/package.json b/package.json
index 6b3d204..c220b60 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pin-github-action",
-  "version": "3.1.1",
+  "version": "3.1.2",
   "description": "Pin your GitHub Actions to specific versions automatically!",
   "exports": "./index.js",
   "type": "module",
diff --git a/replaceActions.js b/replaceActions.js
index 1c76385..fe674ec 100644
--- a/replaceActions.js
+++ b/replaceActions.js
@@ -12,7 +12,7 @@ export default function (input, action, comment) {
   const regexpUpdate = new RegExp(
     `uses(\\s*):(\\s+)${quotePattern}${escapeStringRegexp(
       actionId,
-    )}@${escapeStringRegexp(currentVersion)}\\3(\\s*)#[^\\n]*`,
+    )}@${escapeStringRegexp(currentVersion)}\\3([\\t ]*)#[^\\n]*`,
     "g",
   );
 


Test patch:
diff --git a/findRefOnGithub.test.js b/findRefOnGithub.test.js
index 689c5f4..fa35a02 100644
--- a/findRefOnGithub.test.js
+++ b/findRefOnGithub.test.js
@@ -88,8 +88,9 @@ test("fails to find ref (rate limiting)", () => {
   mockRefLookupFailure(action, "tags/master");
   mockRefLookupFailure(action, "heads/master");
   mockCommitLookupRateLimit(action, "master");
+  const resetDate = new Date(1744211324000).toLocaleString();
   return expect(findRef(action)).rejects.toEqual(
-    `Unable to find SHA for nexmo/github-actions@master\nAPI rate limit exceeded for 1.2.3.4. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)`,
+    `Unable to find SHA for nexmo/github-actions@master\nAPI rate limit exceeded for 1.2.3.4. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.) Limit resets at: ${resetDate}`,
   );
 });
 
@@ -161,5 +162,8 @@ function mockCommitLookupRateLimit(action, commitSha) {
     .reply(
       429,
       "API rate limit exceeded for 1.2.3.4. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
+      {
+        "x-ratelimit-reset": "1744211324",
+      },
     );
 }
diff --git a/replaceActions.test.js b/replaceActions.test.js
index 8b36333..efeef6e 100644
--- a/replaceActions.test.js
+++ b/replaceActions.test.js
@@ -369,6 +369,37 @@ jobs:
   expect(actual).toContain("uses: 'mheap/test-action@sha-two' # v1");
 });
 
+test("does not replace comments on the next line", () => {
+  let input = `name: PR
+on:
+  - pull_request
+jobs:
+  test-job:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: mheap/test-action@master
+
+      # This is a comment that should be untouched
+      - uses: mheap/test-action@master
+`;
+
+  let expected = `name: PR
+on:
+  - pull_request
+jobs:
+  test-job:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: mheap/test-action@sha-here # master
+
+      # This is a comment that should be untouched
+      - uses: mheap/test-action@sha-here # master
+`;
+
+  let actual = replaceActions(input, { ...action });
+  expect(actual).toBe(expected);
+});
+
 function convertToYaml(input) {
   return YAML.stringify(input);
 }