Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .github/workflows/cache-evict.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Evict caches for merged PRs

# Caches for merged PRs stay around until we bump against our cache limit.
# They are also not shareable between branches. Therefore, once a PR is
# merged, the cache for that branch is immediately not needed anymore, and we
# can remove it. If we remove these caches, we can save space and be more sure
# that the remaining caches are actually useful, and not at danger of being
# evicted.

on:
workflow_dispatch:
push:
branches:
- main

concurrency:
group: "cache-evict"
cancel-in-progress: true

jobs:
evict-cache:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Evict caches
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: node ./scripts/cache-evict.mjs
18 changes: 18 additions & 0 deletions .github/workflows/cache-warm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Keeps build cache warm

on:
workflow_dispatch:
schedule:
# GitHub evicts unused caches after 7 days, so we run this every 6 days
- cron: "0 0 */6 * *"

concurrency:
group: "cache-warm"
cancel-in-progress: false

jobs:
warm-cache-build:
uses: ./.github/workflows/waspc-build.yaml

warm-cache-ci:
uses: ./.github/workflows/waspc-ci.yaml
3 changes: 3 additions & 0 deletions .github/workflows/deploy-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ concurrency:
group: "deploy-examples"
cancel-in-progress: true

env:
WASP_TELEMETRY_DISABLE: 1

jobs:
deploy:
strategy:
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/waspc-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ on:
pull_request:
paths:
- "waspc/**"
schedule:
# Additionally run once per week (At 00:00 on Sunday) to avoid loosing cache
# (GH deletes it after 7 days of not using it).
- cron: "0 0 * * 0"

env:
WASP_TELEMETRY_DISABLE: 1
Expand Down
118 changes: 118 additions & 0 deletions scripts/cache-evict.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// @ts-check

// This script is intended to be run in a GitHub Actions workflow to delete
// orphaned caches that are no longer associated with any remote refs.
// Called from `.github/workflows/cache-evict.yml`.

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";

const { repository } = assertGithubActionsEnv();
deleteOrphanedCaches(repository);

function deleteOrphanedCaches(/** @type {string} */ githubRepository) {
const ghCaches = listGitHubCaches();
const remoteRefs = listRemoteRefs(githubRepository);

const cachesToDelete = ghCaches.filter((cache) => !remoteRefs.has(cache.ref));
console.log(`Found ${cachesToDelete.length} caches to delete`);

for (const { key, ref } of cachesToDelete) {
deleteGitHubCache(key, ref);
}

console.log("Done");
}

function listGitHubCaches() {
const ghCachesOutput = /** @type {{ key: string, ref: string }[]} */ (
JSON.parse(
runCmd(
// We ask for the output to be a JSON array of {key, ref} objects.
"gh",
["cache", "list", "--limit", "100", "--json", "key,ref"],
),
)
);
console.log(`Found ${ghCachesOutput.length} cache keys`);
return ghCachesOutput;
}

function listRemoteRefs(githubRepository) {
const parsedLsRemoteOutput = parseGitRemoteOutput(
runCmd(
// We use `git ls-remote` so we also receive refs such as `refs/pull/123/head`
// which are not downloaded by `git fetch` or `git pull`.
"git",
["ls-remote", `https://github.com/${githubRepository}.git`],
),
);

const remoteRefs = new Set(
parsedLsRemoteOutput.map(({ gitRefName }) => gitRefName),
);

console.log(`Found ${remoteRefs.size} remote refs`);
return remoteRefs;
}

function deleteGitHubCache(
/** @type {string} */ key,
/** @type {string} */ ref,
) {
try {
console.group(`Deleting cache "${key}" for ref "${ref}"`);
runCmd("gh", ["cache", "delete", key], { collectStdout: false });
console.log(`Done`);
} catch (e) {
console.warn(`::warning::Failed to delete cache key ${key}`);
} finally {
console.groupEnd();
}
}

function assertGithubActionsEnv() {
const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY;
assert(
GITHUB_REPOSITORY,
"GITHUB_REPOSITORY environment variable is required. This environment variable is typically set by GitHub Actions.",
);

const ghVersion = runCmd("gh", ["--version"]).trim();
const gitVersion = runCmd("git", ["--version"]).trim();

console.group("Environment");
console.log(`GITHUB_REPOSITORY: ${GITHUB_REPOSITORY}`);
console.log(`gh version: ${ghVersion}`);
console.log(`git version: ${gitVersion}`);
console.groupEnd();

return { repository: GITHUB_REPOSITORY, ghVersion, gitVersion };
}

function runCmd(
/** @type {string} */ cmd,
/** @type {string[]} */ args,
{ collectStdout = true } = {},
) {
return execFileSync(cmd, args, {
encoding: "utf-8",
stdio:
// stdin, stdout, stderr
["ignore", collectStdout ? "pipe" : "inherit", "inherit"],
});
}

function parseGitRemoteOutput(/** @type {string} */ str) {
// According to https://git-scm.com/docs/git-ls-remote, the output is:
// <oid> TAB <ref> LF
// `oid` being the internal Git object ID, and `ref` the reference name

return str
.trim()
.split("\n")
.map((line) => {
const [gitObjectId, gitRefName] = line.split("\t");
return { gitObjectId, gitRefName };
});
}