Skip to content

GitHub Action to cache a directory via rsync#7

Merged
pjones merged 3 commits into
mainfrom
pjones/cache-action
Jul 16, 2026
Merged

GitHub Action to cache a directory via rsync#7
pjones merged 3 commits into
mainfrom
pjones/cache-action

Conversation

@pjones

@pjones pjones commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Simple directory sync via rsync.

Summary by CodeRabbit

  • New Features
    • Added an rsync-based GitHub Action to restore/push SSH-backed cache for build artifacts.
  • Documentation
    • Updated the README with CI/CD usage, including a “Cache via rsync” example and SSH-key behavior notes.
  • Tests
    • Added a Node test suite covering cache restore/push/disabled behavior.
  • Chores
    • Updated environment/ignore examples, introduced Nix flake + dev tooling, and added build/test scripts with Rollup bundling.
    • Enhanced the CI workflow to validate tests/build outputs and ensure generated files stay in sync.

@pjones
pjones requested review from a team and timosachsenberg June 26, 2026 13:42
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pjones, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 033d82c2-32e4-4a59-95af-24d596cea80d

📥 Commits

Reviewing files that changed from the base of the PR and between 0b014e4 and 469e7f7.

⛔ Files ignored due to path filters (4)
  • actions/rsync-cache/dist/commit.js is excluded by !**/dist/**
  • actions/rsync-cache/dist/commit.js.map is excluded by !**/dist/**, !**/*.map
  • actions/rsync-cache/dist/restore.js is excluded by !**/dist/**
  • actions/rsync-cache/dist/restore.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (7)
  • .github/workflows/test.yml
  • README.md
  • actions/rsync-cache/action.yml
  • actions/rsync-cache/src/rsync.js
  • actions/rsync-cache/test/rsync_test.js
  • flake.nix
  • package.json
📝 Walkthrough

Walkthrough

Adds a new Cache via rsync GitHub Action, its rsync-based restore and commit entrypoints, bundle build configuration, tests, CI validation, and README usage instructions.

Changes

Cache via rsync action

Layer / File(s) Summary
Build and development environment
package.json, rollup.config.js, flake.nix, .envrc.example, .gitignore
Adds ES module package metadata, Rollup bundle configuration, a Nix development shell, and local environment ignore rules.
Rsync transport implementation
actions/rsync-cache/src/rsync.js
Introduces the Rsync class with input validation, SSH and rsync command construction, key-file handling, disabled-cache behavior, and pull()/push() synchronization.
Action metadata and entrypoints
actions/rsync-cache/action.yml, actions/rsync-cache/src/restore.js, actions/rsync-cache/src/commit.js
Defines action inputs and Node 24 execution, wiring restore and successful-job post steps to Rsync.pull() and Rsync.push().
Validation and usage integration
actions/rsync-cache/test/rsync_test.js, .github/workflows/test.yml, README.md
Tests command construction and cache behavior, checks tests and generated bundles in CI, and documents build and workflow usage.

Sequence Diagram(s)

sequenceDiagram
  participant GitHubAction
  participant restore.js
  participant Rsync
  participant rsync
  participant commit.js
  GitHubAction->>restore.js: start restore step
  restore.js->>Rsync: pull()
  Rsync->>rsync: sync remote cache to local path
  rsync-->>Rsync: exit code
  GitHubAction->>commit.js: run post step after success
  commit.js->>Rsync: push()
  Rsync->>rsync: sync local path to remote cache
  rsync-->>Rsync: exit code
Loading

Poem

A bunny packed the cache with care,
And sent it hopping through the air.
Restore, then push, the bundles shine—
Rsync keeps the paths in line.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: a GitHub Action that caches a directory using rsync.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pjones/cache-action

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
actions/rsync-cache/src/rsync.js (1)

71-79: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Private key file is not cleaned up if rsync invocation throws.

exec.exec runs with ignoreReturnCode: true, so a non-zero exit won't throw, but any thrown error (e.g., exec itself failing) leaves the private key at this.ssh_key_file on disk. Wrap the run/unlink in try/finally to guarantee removal.

🛡️ Proposed fix
-    // Run rsync.
-    const options = {ignoreReturnCode: true};
-    const exit_code = await exec.exec(this.command, [...this.args, ...additional_arguments], options);
-
-    // Remove the key file.
-    await fs.unlink(this.ssh_key_file);
-
-    // Let callers decide what to do with the exit code.
-    return exit_code;
+    // Run rsync.
+    const options = {ignoreReturnCode: true};
+    try {
+      return await exec.exec(this.command, [...this.args, ...additional_arguments], options);
+    } finally {
+      // Always remove the key file.
+      await fs.unlink(this.ssh_key_file);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actions/rsync-cache/src/rsync.js` around lines 71 - 79, The rsync command
path in rsync.js does not guarantee cleanup of the temporary SSH key when
exec.exec throws. Update the run/unlink flow in the rsync method to use a
try/finally around the exec.exec call so that fs.unlink(this.ssh_key_file)
always runs, even if this.command or the exec invocation fails; keep the
existing ignoreReturnCode behavior and preserve the current key file
creation/chmod logic.
flake.nix (1)

19-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the action's runtime tools to the dev shell.

nix develop currently gives you Node and shell linters, but this action's core path shells out through rsync and SSH. Including those binaries here would make local smoke-testing much closer to the real runtime.

Suggested diff
           devShells.default = pkgs.mkShell {
             nativeBuildInputs = with pkgs; [
               nodejs_24
+              rsync
+              openssh
               shellcheck
               shfmt
             ];
           };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flake.nix` around lines 19 - 24, The dev shell in the mkShell setup is
missing the runtime binaries used by the action’s main path, so local testing
does not match production behavior. Update the nativeBuildInputs for
devShells.default to include the tools used by the action’s rsync and SSH
execution path, alongside nodejs_24, shellcheck, and shfmt, so nix develop
provides everything needed for realistic smoke tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@actions/rsync-cache/src/rsync.js`:
- Around line 20-24: The rsync target is missing the configured SSH username, so
`ssh_user` is read in the `RsyncCache` constructor but never applied. Update the
SSH/rsync command assembly in `RsyncCache` (including the `ssh_dir` target and
the command built later in the class) to prefix the remote host with
`this.ssh_user` so connections authenticate against the intended account instead
of the local runner user.
- Around line 76-82: `run()` in `RsyncCache` has two breakages: it is passing
merged arguments incorrectly and returning an undefined exit code. Update the
`exec.exec` call to combine `this.args` with `additional_arguments` as an array
so `this.command` receives valid args, and fix the exit-code variable naming so
the value assigned from `exec.exec` is the same symbol returned later
(`exit_code` instead of the misspelled temporary). Ensure the method still
removes `this.ssh_key_file` before returning.

In `@README.md`:
- Line 28: The ssh_port example has invalid YAML because it includes an extra
PORT: prefix in the value assignment. Update the README example so ssh_port is
defined as a normal key-value pair using the ARCHIVE_RRSYNC_PORT secret, and
remove the extraneous prefix to keep the YAML well-formed.

---

Nitpick comments:
In `@actions/rsync-cache/src/rsync.js`:
- Around line 71-79: The rsync command path in rsync.js does not guarantee
cleanup of the temporary SSH key when exec.exec throws. Update the run/unlink
flow in the rsync method to use a try/finally around the exec.exec call so that
fs.unlink(this.ssh_key_file) always runs, even if this.command or the exec
invocation fails; keep the existing ignoreReturnCode behavior and preserve the
current key file creation/chmod logic.

In `@flake.nix`:
- Around line 19-24: The dev shell in the mkShell setup is missing the runtime
binaries used by the action’s main path, so local testing does not match
production behavior. Update the nativeBuildInputs for devShells.default to
include the tools used by the action’s rsync and SSH execution path, alongside
nodejs_24, shellcheck, and shfmt, so nix develop provides everything needed for
realistic smoke tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50cc7193-63a9-4551-9afb-7949237892fe

📥 Commits

Reviewing files that changed from the base of the PR and between 42eac09 and c94f115.

⛔ Files ignored due to path filters (2)
  • flake.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .envrc.example
  • .gitignore
  • README.md
  • actions/rsync-cache/action.yml
  • actions/rsync-cache/src/commit.js
  • actions/rsync-cache/src/restore.js
  • actions/rsync-cache/src/rsync.js
  • flake.nix
  • package.json
  • rollup.config.js

Comment thread actions/rsync-cache/src/rsync.js Outdated
Comment thread actions/rsync-cache/src/rsync.js Outdated
Comment thread README.md Outdated
Comment thread actions/rsync-cache/src/rsync.js Outdated
@pjones
pjones force-pushed the pjones/cache-action branch 2 times, most recently from 6cb0531 to dd072f3 Compare June 26, 2026 14:24
@pjones

pjones commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@castorNova2

This is the caching action I wrote for vcpkg.

@pjones
pjones force-pushed the pjones/cache-action branch 2 times, most recently from ca62d9b to 66d0d97 Compare June 26, 2026 14:34
Probably needs more error handling.
@pjones
pjones force-pushed the pjones/cache-action branch from 66d0d97 to 9e99f4b Compare June 26, 2026 14:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
actions/rsync-cache/src/rsync.js (2)

45-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer endsWith for clarity.

Using String.prototype.endsWith() is more direct, robust, and readable than a regular expression for checking a trailing slash.

♻️ Proposed refactor
-    if (!this.path.match(/\/$/)) {
+    if (!this.path.endsWith("/")) {
       this.path += "/";
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actions/rsync-cache/src/rsync.js` around lines 45 - 47, Update the
trailing-slash check in the path normalization logic to use
String.prototype.endsWith("/") instead of the regular expression, while
preserving the existing behavior of appending a slash only when this.path does
not already end with one.

29-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the empty-string check.

Using .trim() simplifies this check by explicitly conveying the intent and avoiding a regular expression.

♻️ Proposed refactor
-    if (
-      !this.ssh_key ||
-      this.ssh_key.length == 0 ||
-      this.ssh_key.match(/^\s*$/)
-    ) {
+    if (!this.ssh_key || this.ssh_key.trim().length === 0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actions/rsync-cache/src/rsync.js` around lines 29 - 33, Update the ssh_key
validation condition in the surrounding method to use trim-based whitespace
checking instead of ssh_key.match(/^\s*$/), while preserving the existing
handling for missing, empty, and whitespace-only keys.
.github/workflows/test.yml (2)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider explicitly setting the Node.js version.

The workflow runs npm run test and npm run build using the runner's default Node.js version. Since this action is designed for the Node 24 execution lifecycle, consider using actions/setup-node to explicitly test against Node 24 and ensure consistency.

♻️ Proposed refactor
           set -eu
 
+          # Set up Node.js
+          # - uses: actions/setup-node@v4
+          #   with:
+          #     node-version: '24'
+
           # Run all of the tests.
           npm run test

(Note: You will need to add the uses step before the run step block if you decide to implement this).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 20 - 22, Add an actions/setup-node
step before the test and build commands, explicitly configuring Node.js 24 so
both npm run test and npm run build use the intended runtime.

17-17: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false to improve security posture.

As highlighted by static analysis, when actions/checkout is used without persist-credentials: false, the repository GITHUB_TOKEN is persisted in the local git config, which can be inadvertently exposed to subsequent steps. Disabling this reduces the risk of credential leakage.

🛡️ Proposed refactor
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml at line 17, Update the actions/checkout step in
the workflow to set persist-credentials to false, ensuring the repository token
is not stored in local Git configuration while preserving the existing checkout
behavior.

Source: Linters/SAST tools

actions/rsync-cache/test/rsync_test.js (1)

57-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider throwing an Error object instead of a string.

Throwing an Error object preserves the stack trace and aligns with best practices for error handling in JavaScript.

♻️ Proposed refactor
         if (options["required"] && !val) {
-          throw `missing required input: ${key}`;
+          throw new Error(`missing required input: ${key}`);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actions/rsync-cache/test/rsync_test.js` around lines 57 - 59, Update the
required-input validation in the options handling block to throw an Error object
with the existing “missing required input” message instead of throwing a string,
preserving the current validation condition and message content.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@actions/rsync-cache/src/rsync.js`:
- Around line 25-26: Update the SSH key path initialization in the constructor
to generate a unique key filename for each execution instead of always using
private.key, while preserving the existing ~/.ssh directory location and
ensuring all subsequent key operations use the generated path.
- Around line 102-118: Wrap the rsync execution and key-file cleanup in the
method containing the shown flow with a try...finally block, ensuring
this.fs.unlink(this.ssh_key_file) runs even when this.fs.chmod or this.exec.exec
throws. Use return await for the exec result inside the try block so the finally
cleanup waits for command completion, while preserving the existing exit_code
return behavior.

In `@actions/rsync-cache/test/rsync_test.js`:
- Around line 192-205: Replace the async forEach callback in the “disabled on
missing ssh key” test with an awaited sequential loop, preserving the existing
key values and assertions so each rsync.run() completes before the test
finishes.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 20-22: Add an actions/setup-node step before the test and build
commands, explicitly configuring Node.js 24 so both npm run test and npm run
build use the intended runtime.
- Line 17: Update the actions/checkout step in the workflow to set
persist-credentials to false, ensuring the repository token is not stored in
local Git configuration while preserving the existing checkout behavior.

In `@actions/rsync-cache/src/rsync.js`:
- Around line 45-47: Update the trailing-slash check in the path normalization
logic to use String.prototype.endsWith("/") instead of the regular expression,
while preserving the existing behavior of appending a slash only when this.path
does not already end with one.
- Around line 29-33: Update the ssh_key validation condition in the surrounding
method to use trim-based whitespace checking instead of ssh_key.match(/^\s*$/),
while preserving the existing handling for missing, empty, and whitespace-only
keys.

In `@actions/rsync-cache/test/rsync_test.js`:
- Around line 57-59: Update the required-input validation in the options
handling block to throw an Error object with the existing “missing required
input” message instead of throwing a string, preserving the current validation
condition and message content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16fc13c5-69a4-4b2e-bd73-c766f4845f16

📥 Commits

Reviewing files that changed from the base of the PR and between c94f115 and 0b014e4.

⛔ Files ignored due to path filters (4)
  • actions/rsync-cache/dist/commit.js is excluded by !**/dist/**
  • actions/rsync-cache/dist/commit.js.map is excluded by !**/dist/**, !**/*.map
  • actions/rsync-cache/dist/restore.js is excluded by !**/dist/**
  • actions/rsync-cache/dist/restore.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (10)
  • .github/workflows/test.yml
  • README.md
  • actions/rsync-cache/action.yml
  • actions/rsync-cache/src/commit.js
  • actions/rsync-cache/src/restore.js
  • actions/rsync-cache/src/rsync.js
  • actions/rsync-cache/test/rsync_test.js
  • flake.nix
  • package.json
  • rollup.config.js
🚧 Files skipped from review as they are similar to previous changes (7)
  • rollup.config.js
  • actions/rsync-cache/src/restore.js
  • actions/rsync-cache/src/commit.js
  • actions/rsync-cache/action.yml
  • flake.nix
  • package.json
  • README.md

Comment thread actions/rsync-cache/src/rsync.js Outdated
Comment thread actions/rsync-cache/src/rsync.js Outdated
Comment thread actions/rsync-cache/test/rsync_test.js
@pjones
pjones force-pushed the pjones/cache-action branch from 0b014e4 to 22e5ba5 Compare July 16, 2026 11:20
@pjones
pjones force-pushed the pjones/cache-action branch from 22e5ba5 to 469e7f7 Compare July 16, 2026 11:36
@pjones
pjones requested a review from timosachsenberg July 16, 2026 11:43
@pjones

pjones commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@timosachsenberg I added tests and finished this up. If you approve we can start using it.

@timosachsenberg

Copy link
Copy Markdown

give me a minute... looking at it now

@timosachsenberg

Copy link
Copy Markdown

Code looks solid. Thanks!
ai flagged: package.json deps — add @actions/io (directly imported but only transitively resolved), drop the unused @actions/github. which is probably real but not urgent
feel free to merge

@pjones
pjones merged commit e27e306 into main Jul 16, 2026
3 checks passed
@pjones

pjones commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@castorNova2 this caching action is ready to use with vcpkg.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants