Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: support arbitrary mount options for each cache
Signed-off-by: Amin Yahyaabadi <[email protected]>
  • Loading branch information
aminya committed Apr 2, 2024
commit 43a181df387b71d5bb72df611c8c65d26510a257
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ Real-world examples:
- <https://github.com/rootless-containers/slirp4netns/blob/v1.2.2/.github/workflows/release.yaml#L18-L36>
- <https://github.com/containers/fuse-overlayfs/blob/40e0f3c/.github/workflows/release.yaml#L17-L36>

## CacheMap Options

Optionally, instead of a single string for the `target`, you can provide an object with additional options that should be passed to `--mount=type=cache` in the values `cache-map` JSON. The `target` path must be present in the object as a property.

```json
{
"var-cache-apt": {
"target": "/var/cache/apt",
"sharing": "locked",
"id": "1"
},
"var-lib-apt": "/var/lib/apt"
}
```

## CLI Usage

In other CI systems, you can run the script directly via `node`:
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: "Injects the cached data into the docker build(x|kit) process"
inputs:
cache-map:
required: true
description: "The map of actions source to container destination paths for the cache paths"
description: "The map of actions source paths to container destination paths or mount arguments"
cache-source:
deprecationMessage: "Use `cache-map` instead"
description: "Where the cache is stored in the calling workspace. Default: `cache`"
Expand Down
41 changes: 32 additions & 9 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions src/extract-cache.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import fs from 'fs/promises';
import path from 'path';
import { Opts, getCacheMap } from './opts.js';
import { CacheOptions, Opts, getCacheMap, getMountArgsString, getTargetPath } from './opts.js';
import { run, runPiped } from './run.js';
import { spawn } from 'child_process';

async function extractCache(cacheSource: string, cacheTarget: string, scratchDir: string) {
async function extractCache(cacheSource: string, cacheOptions: CacheOptions, scratchDir: string) {
// Prepare Timestamp for Layer Cache Busting
const date = new Date().toISOString();
await fs.writeFile(path.join(scratchDir, 'buildstamp'), date);

// Prepare Dancefile to Access Caches
const targetPath = getTargetPath(cacheOptions);
const mountArgs = getMountArgsString(cacheOptions);

const dancefileContent = `
FROM busybox:1
COPY buildstamp buildstamp
RUN --mount=type=cache,target=${cacheTarget} \
RUN --mount=${mountArgs} \
mkdir -p /var/dance-cache/ \
&& cp -p -R ${cacheTarget}/. /var/dance-cache/ || true
&& cp -p -R ${targetPath}/. /var/dance-cache/ || true
`;
await fs.writeFile(path.join(scratchDir, 'Dancefile.extract'), dancefileContent);
console.log(dancefileContent);
Expand Down Expand Up @@ -52,7 +55,7 @@ export async function extractCaches(opts: Opts) {
const scratchDir = opts['scratch-dir'];

// Extract Caches for each source-target pair
for (const [cacheSource, cacheTarget] of Object.entries(cacheMap)) {
await extractCache(cacheSource, cacheTarget, scratchDir);
for (const [cacheSource, cacheOptions] of Object.entries(cacheMap)) {
await extractCache(cacheSource, cacheOptions, scratchDir);
}
}
15 changes: 9 additions & 6 deletions src/inject-cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs from 'fs/promises';
import path from 'path';
import { Opts, getCacheMap } from './opts.js';
import { CacheOptions, Opts, getCacheMap, getMountArgsString, getTargetPath } from './opts.js';
import { run } from './run.js';
import { notice } from '@actions/core';

async function injectCache(cacheSource: string, cacheTarget: string, scratchDir: string) {
async function injectCache(cacheSource: string, cacheOptions: CacheOptions, scratchDir: string) {
// Clean Scratch Directory
await fs.rm(scratchDir, { recursive: true, force: true });
await fs.mkdir(scratchDir, { recursive: true });
Expand All @@ -16,13 +16,16 @@ async function injectCache(cacheSource: string, cacheTarget: string, scratchDir:
const date = new Date().toISOString();
await fs.writeFile(path.join(cacheSource, 'buildstamp'), date);

const targetPath = getTargetPath(cacheOptions);
const mountArgs = getMountArgsString(cacheOptions);

// Prepare Dancefile to Access Caches
const dancefileContent = `
FROM busybox:1
COPY buildstamp buildstamp
RUN --mount=type=cache,target=${cacheTarget} \
RUN --mount=${mountArgs} \
--mount=type=bind,source=.,target=/var/dance-cache \
cp -p -R /var/dance-cache/. ${cacheTarget} || true
cp -p -R /var/dance-cache/. ${targetPath} || true
`;
await fs.writeFile(path.join(scratchDir, 'Dancefile.inject'), dancefileContent);
console.log(dancefileContent);
Expand All @@ -45,7 +48,7 @@ export async function injectCaches(opts: Opts) {
const scratchDir = opts['scratch-dir'];

// Inject Caches for each source-target pair
for (const [cacheSource, cacheTarget] of Object.entries(cacheMap)) {
await injectCache(cacheSource, cacheTarget, scratchDir);
for (const [cacheSource, cacheOptions] of Object.entries(cacheMap)) {
await injectCache(cacheSource, cacheOptions, scratchDir);
}
}
43 changes: 40 additions & 3 deletions src/opts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,54 @@ Save 'RUN --mount=type=cache' caches on GitHub Actions or other CI platforms

Options:
--extract Extract the cache from the docker container (extract step). Otherwise, inject the cache (main step)
--cache-map The map of actions source to container destination paths for the cache paths
--cache-map The map of actions source paths to container destination paths or mount arguments
--scratch-dir Where the action is stores some temporary files for its processing. Default: 'scratch'
--skip-extraction Skip the extraction of the cache from the docker container
--help Show this help
`);
}

export function getCacheMap(opts: Opts): Record<string, string> {
export type SourcePath = string
export type TargetPath = string
export type ToStringable = {
toString(): string;
}
export type CacheOptions = TargetPath | { target: TargetPath } & Record<string, ToStringable>
export type CacheMap = Record<SourcePath, CacheOptions>

export function getCacheMap(opts: Opts): CacheMap {
try {
return JSON.parse(opts["cache-map"]) as Record<string, string>;
return JSON.parse(opts["cache-map"]) as CacheMap;
} catch (e) {
throw new Error(`Failed to parse cache map. Expected JSON, got:\n${opts["cache-map"]}\n${e}`);
}
}

export function getTargetPath(cacheOptions: CacheOptions): TargetPath {
if (typeof cacheOptions === "string") {
// only the target path is provided
return cacheOptions;
} else {
// object is provided
try {
return cacheOptions.target;
} catch (e) {
throw new Error(`Expected the 'target' key in the cache options, got:\n${cacheOptions}\n${e}`);
}
}
}

/**
* Convert a cache options to a string that is passed to --mount=
* @param CacheOptions The cache options to convert to a string
*/
export function getMountArgsString(cacheOptions: CacheOptions): string {
if (typeof cacheOptions === "string") {
// only the target path is provided
return `type=cache,target=${cacheOptions}`;
} else {
// other options are provided
const otherOptions = Object.entries(cacheOptions).map(([key, value]) => `${key}=${value}`).join(",");
return `type=cache,${otherOptions}`;
}
}