Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ builds something different its own `key`:
key: ${{ matrix.target }}
```

### Share an existing job's cache

Compatible jobs in different workflows can reuse an existing cache by naming
its owning workflow and job. Use the same `key` and `working-directory`, and
keep the target, toolchain and build flags compatible:

```yaml
- uses: tensorlakeai/github-runners@v1
with:
languages: rust
cache-owner: build_platform_dataplane_on_merge.yaml/build_binary
key: dataplane-platform-dataplane-release-glibc-2.34-x86_64-gnu-and-musl
```

The owner job needs no configuration change. Existing entries are reused;
branches and pull requests still save only to their own ref. Builds restore
archives to local disk and do not share a writable build directory.

## What is cached

| Language | Detected from | Cached |
Expand Down Expand Up @@ -195,6 +213,7 @@ of many small files.
| `languages` | `auto` | `auto`, `none`, or a comma list of `rust`, `node`, `go`, `python`. |
| `paths` | | Extra files or directories to cache, one per line, relative to `working-directory` or starting with `~/`. |
| `key` | | Separates caches for jobs that build different things, such as matrix entries. |
| `cache-owner` | | Reuse another job's cache in this repository, as `workflow.yml/job-id`; use its `key` and `working-directory`. |
| `working-directory` | `.` | Where to search for lockfiles. |
| `save` | `auto` | `auto` saves to the job's own branch or pull request when dependencies changed, except in merge queue runs; `true` also saves in merge queue runs; `false` never saves. |
| `default-branch` | the repository's | The fallback cache for branches, and never expired. |
Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ inputs:
build different things, for example the target triple.
required: false
default: ""
cache-owner:
description: >-
Share an existing job's cache in this repository. Use workflow.yml/job-id
and the same key and working-directory as that job. Branch isolation is
preserved. Leave empty to use this workflow and job.
required: false
default: ""
working-directory:
description: Directory to search for lockfiles.
required: false
Expand Down
15 changes: 13 additions & 2 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ function workflowIdentity() {
return ref.replace(/@.*$/, '');
}

function cacheIdentity() {
const owner = core.getInput('cache-owner', '');
if (!owner) return [workflowIdentity(), process.env.GITHUB_JOB || ''];
const match = /^([A-Za-z0-9_.-]+\.ya?ml)\/([A-Za-z_][A-Za-z0-9_-]*)$/.exec(owner);
if (!match) throw new Error('Input cache-owner must be workflow.yml/job-id in this repository.');
const repository = process.env.GITHUB_REPOSITORY || '';
if (!repository) throw new Error('Input cache-owner requires GITHUB_REPOSITORY.');
return [`${repository}/.github/workflows/${match[1]}`, match[2]];
}

function readEvent() {
try {
return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
Expand Down Expand Up @@ -83,10 +93,11 @@ function load() {
const workingDirectory = path.resolve(process.env.GITHUB_WORKSPACE || process.cwd(), core.getInput('working-directory', '.'));
const key = core.getInput('key', '');
const languages = parseLanguages(core.getInput('languages', 'auto'));
const [workflow, job] = cacheIdentity();
const scope = sha256(JSON.stringify([
process.env.GITHUB_REPOSITORY || '',
workflowIdentity(),
process.env.GITHUB_JOB || '',
workflow,
job,
path.relative(process.env.GITHUB_WORKSPACE || process.cwd(), workingDirectory),
key,
])).slice(0, 20);
Expand Down
54 changes: 54 additions & 0 deletions test/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,60 @@ test('looks up the own ref, then the base branch, then the default branch', () =
assert.deepEqual(config.refs(''), ['refs/heads/feature']);
});

test('cache-owner reuses the existing scope and keeps repository and build keys isolated', () => {
job({ inputs: { key: 'release-linux' } });
const existing = config.load().scope;
const legacy = config.sha256(JSON.stringify(['acme/app', 'acme/app/.github/workflows/ci.yml', 'build', '', 'release-linux'])).slice(0, 20);
assert.equal(existing, legacy, 'default scopes remain compatible with existing entries');

job({ inputs: { 'cache-owner': 'ci.yml/build', key: 'release-linux' } });
process.env.GITHUB_WORKFLOW_REF = 'acme/app/.github/workflows/publish.yml@refs/heads/main';
process.env.GITHUB_JOB = 'helper';
assert.equal(config.load().scope, existing);

process.env.INPUT_KEY = 'release-musl';
assert.notEqual(config.load().scope, existing, 'different build keys stay isolated');
process.env.INPUT_KEY = 'release-linux';
process.env.GITHUB_REPOSITORY = 'acme/other';
assert.notEqual(config.load().scope, existing, 'cache-owner cannot cross repositories');
});

test('rejects malformed cache owners', () => {
for (const owner of ['../ci.yml/build', 'acme/app/ci.yml/build', 'ci.yml', 'ci.yml/../build', 'ci.yml/1build']) {
job({ inputs: { 'cache-owner': owner } });
assert.throws(() => config.load(), /cache-owner must be/);
}
job({ inputs: { 'cache-owner': 'ci.yml/build' } });
delete process.env.GITHUB_REPOSITORY;
assert.throws(() => config.load(), /requires GITHUB_REPOSITORY/);
});

test('another workflow restores the owner cache and saves only to its own branch', async () => {
const main = job({ inputs: { key: 'release-linux' } });
rustProject(main);
const dependency = path.join('target', 'debug', 'deps', 'libserde.rlib');
write(path.join(main.workspace, dependency), 'built by the owner workflow');
await main.restore();
await main.save();
const mainEntries = entries('rust');

// Fresh runners use the same absolute repository/home paths. Remove the
// build output and switch workflow identity without changing those paths.
fs.rmSync(path.join(main.workspace, 'target'), { recursive: true });
process.env.GITHUB_WORKFLOW_REF = 'acme/app/.github/workflows/publish.yml@refs/pull/9/merge';
process.env.GITHUB_JOB = 'helper';
process.env.GITHUB_EVENT_NAME = 'pull_request';
process.env.GITHUB_REF = 'refs/pull/9/merge';
process.env.GITHUB_BASE_REF = 'main';
process.env['INPUT_CACHE-OWNER'] = 'ci.yml/build';
assert.equal((await main.restore()).restored, 'rust');
assert.equal(fs.readFileSync(path.join(main.workspace, dependency), 'utf8'), 'built by the owner workflow');
write(path.join(main.workspace, 'Cargo.lock'), 'version = 4\n# helper dependency change\n');
await main.save();
assert.equal(entries('rust', 'refs/pull/9/merge').length, 1);
assert.deepEqual(entries('rust'), mainEntries, 'the shared owner cache keeps default-branch entries intact');
});

test('balances shards by size', () => {
const files = [900, 500, 400, 300, 200, 100].map((size, index) => ({ path: `f${index}`, size }));
const shards = archive.plan(files, 1000);
Expand Down
Loading