Skip to content

Commit

Permalink
Implement copy and copy-immutable strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
oNaiPs committed Nov 22, 2023
1 parent c8c75ec commit eb347d7
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 37 deletions.
127 changes: 118 additions & 9 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Push
on: push

env:
SHARED_CACHE_RUN_KEY: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}

jobs:
tests:
runs-on: ubuntu-latest
Expand All @@ -12,7 +9,41 @@ jobs:
- run: npm install
- run: npm run all

first-run:
test-move-strategy-run1:
runs-on:
group: local-action-cache
steps:
- uses: actions/checkout@v3
- name: 'Run first time without cache'
id: 'first-run'
uses: ./
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-move
- name: Assert output
if: steps.first-run.outputs.cache-hit == 'true'
run: echo "Should not have done cache hit" && exit 1
- name: Write cache file
run: echo "demo-results" > ./demo-output.txt

test-move-strategy-run2:
runs-on:
group: local-action-cache
needs: [test-move-strategy-run1]
steps:
- uses: actions/checkout@v3
- uses: ./
id: 'second-run'
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-move
- name: Assert output
if: steps.second-run.outputs.cache-hit != 'true'
run: echo "Should have hit cache" && exit 1
- name: Check contents
run: test $(cat ./demo-output.txt) != "demo-results" && echo "Wrong cached contents $(cat ./demo-output.txt)" && exit 1

test-copy-strategy-run1:
runs-on:
group: local-action-cache
steps:
Expand All @@ -22,26 +53,104 @@ jobs:
uses: ./
with:
path: './demo-output.txt'
key: ${{ env.SHARED_CACHE_RUN_KEY }}
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy
strategy: copy
- name: Assert output
if: steps.first-run.outputs.cache-hit == 'true'
run: echo "Should not have done cache hit" && exit 1
- name: Write cache file
run: echo "demo-results" > ./demo-output.txt

second-run:
test-copy-strategy-run2:
runs-on:
group: local-action-cache
needs: [first-run]
needs: [test-copy-strategy-run1]
steps:
- uses: actions/checkout@v3
- uses: ./
id: 'second-run'
with:
path: './demo-output.txt'
key: ${{ env.SHARED_CACHE_RUN_KEY }}
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy
strategy: copy-immutable
- name: Assert output
if: steps.second-run.outputs.cache-hit != 'true'
run: echo "Should have hit cache" && exit 1
- name: Check contents
run: cat ./demo-output.txt
run: test $(cat ./demo-output.txt) != "demo-results" && echo "Wrong cached contents $(cat ./demo-output.txt)" && exit 1
- name: Change contents
run: echo "changed-results" > ./demo-output.txt

test-copy-strategy-run3:
runs-on:
group: local-action-cache
needs: [test-copy-strategy-run2]
steps:
- uses: actions/checkout@v3
- uses: ./
id: 'third-run'
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy
strategy: copy
- name: Assert output
if: steps.third-run.outputs.cache-hit != 'true'
run: echo "Should have hit cache" && exit 1
- name: Check contents
run: test $(cat ./demo-output.txt) != "changed-results" && echo "Wrong cached contents $(cat ./demo-output.txt)" && exit 1

test-copy-strategy-immutable-run1:
runs-on:
group: local-action-cache
steps:
- uses: actions/checkout@v3
- name: 'Run first time without cache'
id: 'first-run'
uses: ./
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy-immutable
strategy: copy-immutable
- name: Assert output
if: steps.first-run.outputs.cache-hit == 'true'
run: echo "Should not have done cache hit" && exit 1
- name: Write cache file
run: echo "demo-results" > ./demo-output.txt

test-copy-strategy-immutable-run2:
runs-on:
group: local-action-cache
needs: [test-copy-strategy-immutable-run1]
steps:
- uses: actions/checkout@v3
- uses: ./
id: 'second-run'
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy-immutable
strategy: copy-immutable
- name: Assert output
if: steps.second-run.outputs.cache-hit != 'true'
run: echo "Should have hit cache" && exit 1
- name: Check contents
run: test $(cat ./demo-output.txt) != "demo-results" && echo "Wrong cached contents $(cat ./demo-output.txt)" && exit 1
- name: Change contents
run: echo "changed-results" > ./demo-output.txt

test-copy-strategy-immutable-run3:
runs-on:
group: local-action-cache
needs: [test-copy-strategy-immutable-run2]
steps:
- uses: actions/checkout@v3
- uses: ./
id: 'third-run'
with:
path: './demo-output.txt'
key: CACHE_V1-${{ github.run_id }}-${{ github.run_attempt }}-copy-immutable
strategy: copy-immutable
- name: Assert output
if: steps.third-run.outputs.cache-hit != 'true'
run: echo "Should have hit cache" && exit 1
- name: Check contents
run: test $(cat ./demo-output.txt) != "demo-results" && echo "Wrong cached contents $(cat ./demo-output.txt)" && exit 1
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ jobs:
The `key` param is optional and you can use it to invalidate cache.

### Caching strategy

Define how you want your cache to be used. `move` is the default strategy:

| Usage | Description | Observations |
| -------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `strategy: move` | Moves cache on job start, moves back on job end | Uses the filesystem move syscalls, cache saving and restoration is instant. Does not work across disks/remote shares. |
| `strategy: copy` | Recursiverly copies cache on job start, refreshes cache on job end | Works across disks/remote shares. |
| `strategy: copy-immutable` | Recursiverly copies cache on job start, does not refresh cache on job end | Works across disks/remote shares. Faster to use if the contents will never change (e.g. installing node dependencies) |

Refer to [push.yaml](.github/workflows/push.yaml) for examples of how to use the above strategies.

# How it works

The first time `action-local-cache` is used in a runner it will take the given path and create a folder structure inside the `$RUNNER_TOOL_CACHE` dir (usually `_work/_tool` inside the runner's workspace), prefixing the user/org name, repo name, and key.
Expand All @@ -44,11 +56,11 @@ For example, when running the usage example above inside a workflow for the `Mas

Since there's no `api/node_modules/` dir inside, the restore step is skipped and the `cache-hit` output variable will be set to `false`; because of that, the next step which install the dependencies will run.

After the job is successfully completed, `action-local-cache` will take the `api/node_modules` dir generated by the install dependencies step and move it to the previously created dir structure before the runner workspace is wiped.
After the job is successfully completed, `action-local-cache` will take the `api/node_modules` dir generated by the install dependencies step and move/copy it to the previously created dir structure before the runner workspace is wiped.

On a next run under the same runner instance, the cache will be moved back to the working directory and the `cache-output` variable will be set to `true`.
On a next run under the same runner instance, the cache will be moved/copied back to the working directory and the `cache-output` variable will be set to `true`.

After all steps complete the folder will be moved back to the cache dir and so on. Since it is using the filesystem move syscalls, cache saving and restoration is instant.
After all steps complete the folder will be moved/copied back to the cache dir and so on.

# LICENSE

Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ inputs:
path:
description: 'The file or folder to be cached'
required: true
strategy:
description: 'Caching mechanism to be used'
required: false
default: 'move'
outputs:
cache-hit:
description: 'A boolean value to indicate if cache was found and restored'
Expand Down
30 changes: 25 additions & 5 deletions dist/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2443,7 +2443,7 @@ var require_io = __commonJS({
var assert_1 = __require("assert");
var path2 = __importStar(__require("path"));
var ioUtil = __importStar(require_io_util());
function cp(source, dest, options = {}) {
function cp2(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { force, recursive, copySourceDirectory } = readCopyOptions(options);
const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null;
Expand All @@ -2469,7 +2469,7 @@ var require_io = __commonJS({
}
});
}
exports.cp = cp;
exports.cp = cp2;
function mv2(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
if (yield ioUtil.exists(dest)) {
Expand Down Expand Up @@ -2869,6 +2869,12 @@ var import_io_util = __toESM(require_io_util());
var core = __toESM(require_core());
var { GITHUB_REPOSITORY, RUNNER_TOOL_CACHE } = process.env;
var CWD = process.cwd();
var Strategy = /* @__PURE__ */ ((Strategy2) => {
Strategy2["CopyImmutable"] = "copy-immutable";
Strategy2["Copy"] = "copy";
Strategy2["Move"] = "move";
return Strategy2;
})(Strategy || {});
var getVars = () => {
if (!RUNNER_TOOL_CACHE) {
throw new TypeError("Expected RUNNER_TOOL_CACHE environment variable to be defined.");
Expand All @@ -2878,10 +2884,13 @@ var getVars = () => {
}
const options = {
key: core.getInput("key") || "no-key",
path: core.getInput("path")
path: core.getInput("path"),
strategy: core.getInput("strategy")
};
if (!options.path) {
throw new TypeError("path is required but was not provided.");
} else if (!Object.values(Strategy).includes(options.strategy)) {
throw new TypeError(`Unknown strategy ${options.strategy}`);
}
const cacheDir = path__default.default.join(RUNNER_TOOL_CACHE, GITHUB_REPOSITORY, options.key);
const cachePath = path__default.default.join(cacheDir, options.path);
Expand Down Expand Up @@ -2922,8 +2931,19 @@ async function main() {
const { cachePath, targetDir, targetPath, options } = getVars();
if (await (0, import_io_util.exists)(cachePath)) {
await (0, import_io.mkdirP)(targetDir);
await (0, import_io.mv)(cachePath, targetPath, { force: true });
log_default.info(`Cache found and restored to ${options.path}`);
switch (options.strategy) {
case "copy-immutable" /* CopyImmutable */:
case "copy" /* Copy */:
await (0, import_io.cp)(cachePath, targetPath, {
copySourceDirectory: false,
recursive: true
});
break;
case "move" /* Move */:
await (0, import_io.mv)(cachePath, targetPath, { force: true });
break;
}
log_default.info(`Cache found and restored to ${options.path} with ${options.strategy} strategy`);
(0, import_core.setOutput)("cache-hit", true);
} else {
log_default.info(`Skipping: cache not found for ${options.path}.`);
Expand Down
46 changes: 36 additions & 10 deletions dist/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -2270,7 +2270,7 @@ var require_io_util = __commonJS({
exports.IS_WINDOWS = process.platform === "win32";
exports.UV_FS_O_EXLOCK = 268435456;
exports.READONLY = fs.constants.O_RDONLY;
function exists(fsPath) {
function exists2(fsPath) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield exports.stat(fsPath);
Expand All @@ -2283,7 +2283,7 @@ var require_io_util = __commonJS({
return true;
});
}
exports.exists = exists;
exports.exists = exists2;
function isDirectory(fsPath, useStat = false) {
return __awaiter(this, void 0, void 0, function* () {
const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath);
Expand Down Expand Up @@ -2443,7 +2443,7 @@ var require_io = __commonJS({
var assert_1 = __require("assert");
var path2 = __importStar(__require("path"));
var ioUtil = __importStar(require_io_util());
function cp(source, dest, options = {}) {
function cp2(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { force, recursive, copySourceDirectory } = readCopyOptions(options);
const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null;
Expand All @@ -2469,7 +2469,7 @@ var require_io = __commonJS({
}
});
}
exports.cp = cp;
exports.cp = cp2;
function mv2(source, dest, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
if (yield ioUtil.exists(dest)) {
Expand All @@ -2480,7 +2480,7 @@ var require_io = __commonJS({
}
if (destExists) {
if (options.force == null || options.force) {
yield rmRF(dest);
yield rmRF2(dest);
} else {
throw new Error("Destination already exists");
}
Expand All @@ -2491,7 +2491,7 @@ var require_io = __commonJS({
});
}
exports.mv = mv2;
function rmRF(inputPath) {
function rmRF2(inputPath) {
return __awaiter(this, void 0, void 0, function* () {
if (ioUtil.IS_WINDOWS) {
if (/[*"<>|]/.test(inputPath)) {
Expand All @@ -2510,7 +2510,7 @@ var require_io = __commonJS({
}
});
}
exports.rmRF = rmRF;
exports.rmRF = rmRF2;
function mkdirP2(fsPath) {
return __awaiter(this, void 0, void 0, function* () {
assert_1.ok(fsPath, "a path argument must be provided");
Expand Down Expand Up @@ -2868,6 +2868,12 @@ var import_io = __toESM(require_io());
var core = __toESM(require_core());
var { GITHUB_REPOSITORY, RUNNER_TOOL_CACHE } = process.env;
var CWD = process.cwd();
var Strategy = /* @__PURE__ */ ((Strategy2) => {
Strategy2["CopyImmutable"] = "copy-immutable";
Strategy2["Copy"] = "copy";
Strategy2["Move"] = "move";
return Strategy2;
})(Strategy || {});
var getVars = () => {
if (!RUNNER_TOOL_CACHE) {
throw new TypeError("Expected RUNNER_TOOL_CACHE environment variable to be defined.");
Expand All @@ -2877,10 +2883,13 @@ var getVars = () => {
}
const options = {
key: core.getInput("key") || "no-key",
path: core.getInput("path")
path: core.getInput("path"),
strategy: core.getInput("strategy")
};
if (!options.path) {
throw new TypeError("path is required but was not provided.");
} else if (!Object.values(Strategy).includes(options.strategy)) {
throw new TypeError(`Unknown strategy ${options.strategy}`);
}
const cacheDir = path__default.default.join(RUNNER_TOOL_CACHE, GITHUB_REPOSITORY, options.key);
const cachePath = path__default.default.join(cacheDir, options.path);
Expand Down Expand Up @@ -2916,11 +2925,28 @@ if (process.env.LOG_LEVEL) {
var log_default = import_loglevel.default;

// src/post.ts
var import_io_util = __toESM(require_io_util());
async function post() {
try {
const { cacheDir, targetPath, cachePath } = getVars();
const { cacheDir, targetPath, cachePath, options } = getVars();
await (0, import_io.mkdirP)(cacheDir);
await (0, import_io.mv)(targetPath, cachePath, { force: true });
switch (options.strategy) {
case "copy-immutable" /* CopyImmutable */:
if (await (0, import_io_util.exists)(cachePath)) {
log_default.info(`Cache already exists, skipping`);
return;
}
await (0, import_io.cp)(targetPath, cachePath, { copySourceDirectory: true, recursive: true });
break;
case "copy" /* Copy */:
await (0, import_io.rmRF)(cachePath);
await (0, import_io.cp)(targetPath, cachePath, { copySourceDirectory: true, recursive: true });
break;
case "move" /* Move */:
await (0, import_io.mv)(targetPath, cachePath, { force: true });
break;
}
log_default.info(`Cache saved to ${cachePath} with ${options.strategy} strategy`);
} catch (error) {
log_default.trace(error);
(0, import_core.setFailed)(isErrorLike(error) ? error.message : `unknown error: ${error}`);
Expand Down
Loading

0 comments on commit eb347d7

Please sign in to comment.