Merge branch 'worktree-agent-a3b9b1f730f55b5bf'
32 files+556−1016 files not counted0/32 viewed
| 15 | 15 | # desktop-linux the desktop release's Linux build | |
| 16 | 16 | # desktop-windows its Windows cross build, cargo-xwin, the MSVC CRT and | |
| 17 | 17 | # apt's downloads for it | |
| 18 | + | # cli the CLI release's build of every platform, in the | |
| 19 | + | # cargo-zigbuild image | |
| 18 | 20 | # | |
| 19 | 21 | # A job runs only when its key is missing (a new lockfile, a new toolchain, | |
| 20 | 22 | # or an expired entry), and builds what the release builds. Nothing waits for | |
| ⋯ | |||
| 51 | 53 | runner: ${{ steps.missing.outputs.runner }} | |
| 52 | 54 | desktop-linux: ${{ steps.desktop-linux.outputs.cache-hit != 'true' }} | |
| 53 | 55 | desktop-windows: ${{ steps.desktop-windows.outputs.cache-hit != 'true' || steps.xwin.outputs.cache-hit != 'true' || steps.apt-windows.outputs.cache-hit != 'true' }} | |
| 56 | + | cli: ${{ steps.cli.outputs.cache-hit != 'true' }} | |
| 54 | 57 | steps: | |
| 55 | 58 | - uses: actions/checkout@v5 | |
| 56 | 59 | - name: Cache keys | |
| ⋯ | |||
| 135 | 138 | path: ~/.cache/apt/tauri-windows | |
| 136 | 139 | key: apt-tauri-windows-${{ runner.os }}-${{ hashFiles('scripts/apt-install.sh') }} | |
| 137 | 140 | lookup-only: true | |
| 141 | + | - id: cli | |
| 142 | + | uses: actions/cache/restore@v4 | |
| 143 | + | with: | |
| 144 | + | path: | | |
| 145 | + | target/zig | |
| 146 | + | !target/zig/**/incremental | |
| 147 | + | key: cli-release-${{ runner.os }}-${{ steps.keys.outputs.zigbuild }}-${{ steps.keys.outputs.cargo }} | |
| 148 | + | lookup-only: true | |
| 138 | 149 | # The runner's targets to build: those whose build is missing, or all | |
| 139 | 150 | # of them when zig is (each saves it, the first to finish wins). | |
| 140 | 151 | - name: The runner's targets to build | |
| ⋯ | |||
| 352 | 363 | # What the CRT and SDK came to, for the cache's 2 GB an entry may hold. | |
| 353 | 364 | - name: How large the MSVC CRT is | |
| 354 | 365 | run: du -sh ~/.cache/cargo-xwin || true | |
| 366 | + | ||
| 367 | + | # cli-release.yml's build of every platform, in the cargo-zigbuild image | |
| 368 | + | # (scripts/cli-release.mjs). Built whole whenever its key is new (a new | |
| 369 | + | # image or new dependencies), never started from an older entry: what | |
| 370 | + | # zigbuild built cannot be listed by this machine's Cargo to prune it, so | |
| 371 | + | # nothing old is carried in to begin with. | |
| 372 | + | cli: | |
| 373 | + | name: The CLI's release build | |
| 374 | + | needs: look | |
| 375 | + | if: ${{ needs.look.outputs.cli == 'true' }} | |
| 376 | + | # The builder runs in Docker, which g1t's own machines give each job. | |
| 377 | + | runs-on: g1t-4core | |
| 378 | + | timeout-minutes: 60 | |
| 379 | + | steps: | |
| 380 | + | - uses: actions/checkout@v5 | |
| 381 | + | - name: Cache keys | |
| 382 | + | id: keys | |
| 383 | + | run: node scripts/deploy/ci-cache.mjs keys | |
| 384 | + | - name: Cache crates | |
| 385 | + | uses: actions/cache/restore@v4 | |
| 386 | + | with: | |
| 387 | + | path: | | |
| 388 | + | ~/.cargo/registry/index | |
| 389 | + | ~/.cargo/registry/cache | |
| 390 | + | key: cargo-crates-${{ runner.os }}-${{ steps.keys.outputs.cargo }} | |
| 391 | + | restore-keys: cargo-crates-${{ runner.os }}- | |
| 392 | + | - name: Cache the CLI's build | |
| 393 | + | uses: actions/cache@v4 | |
| 394 | + | with: | |
| 395 | + | path: | | |
| 396 | + | target/zig | |
| 397 | + | !target/zig/**/incremental | |
| 398 | + | key: cli-release-${{ runner.os }}-${{ steps.keys.outputs.zigbuild }}-${{ steps.keys.outputs.cargo }} | |
| 399 | + | - name: Build | |
| 400 | + | run: node scripts/cli-release.mjs build | |
| 401 | + | - name: How large the build is | |
| 402 | + | run: du -sh target/zig || true | |
| 8 | 8 | # work, and its header says what a release holds. It is signed with the | |
| 9 | 9 | # runner's release key (RUNNER_RELEASE_KEY, RUNNER_RELEASE_PUBLIC_KEY). | |
| 10 | 10 | # | |
| 11 | − | # Every platform builds in one job, in the rust-cross/cargo-zigbuild image: | |
| 12 | − | # the macOS builds link Apple's Security framework (the keychain), and that | |
| 13 | − | # image carries its SDK. It runs on g1t's own machines (g1t-4core), whose | |
| 14 | − | # jobs have a Docker Engine of their own. | |
| 11 | + | # Every platform builds in one job, in the rust-cross/cargo-zigbuild image, | |
| 12 | + | # pinned by digest in scripts/cli-release.mjs: the macOS builds link Apple's | |
| 13 | + | # Security framework (the keychain), and that image carries its SDK. It | |
| 14 | + | # runs on g1t's own machines (g1t-4core), whose jobs have a Docker Engine of | |
| 15 | + | # their own. | |
| 15 | 16 | name: CLI release | |
| 16 | 17 | ||
| 17 | 18 | on: | |
| ⋯ | |||
| 42 | 43 | run: | | |
| 43 | 44 | version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/g1t/Cargo.toml | head -1)" | |
| 44 | 45 | [ "cli-v$version" = "${GITHUB_REF_NAME}" ] || { echo "::error::The tag is ${GITHUB_REF_NAME}, but crates/g1t is $version"; exit 1; } | |
| 46 | + | # zigbuild: the builder image's digest, whose Rust the build uses; | |
| 47 | + | # cargo: Cargo.lock's crates.io and git packages, not the workspace's | |
| 48 | + | # own versions (scripts/deploy/ci-cache.mjs). | |
| 49 | + | - name: Cache keys | |
| 50 | + | id: keys | |
| 51 | + | run: node scripts/deploy/ci-cache.mjs keys | |
| 52 | + | # Every crate the lockfile names, as CI's Rust job saves them on main. | |
| 53 | + | # The builder reads them from this job's ~/.cargo/registry. | |
| 54 | + | - name: Cache crates | |
| 55 | + | uses: actions/cache/restore@v4 | |
| 56 | + | with: | |
| 57 | + | path: | | |
| 58 | + | ~/.cargo/registry/index | |
| 59 | + | ~/.cargo/registry/cache | |
| 60 | + | key: cargo-crates-${{ runner.os }}-${{ steps.keys.outputs.cargo }} | |
| 61 | + | restore-keys: cargo-crates-${{ runner.os }}- | |
| 62 | + | # Every platform's build. A tag's run restores only from its own tag | |
| 63 | + | # and from main, so the Caches workflow (caches.yml) builds and saves | |
| 64 | + | # this on main whenever the key is new; a release compiles the CLI's | |
| 65 | + | # own crates again, and saves nothing. | |
| 66 | + | - name: Cache the CLI's build | |
| 67 | + | uses: actions/cache/restore@v4 | |
| 68 | + | with: | |
| 69 | + | path: | | |
| 70 | + | target/zig | |
| 71 | + | !target/zig/**/incremental | |
| 72 | + | key: cli-release-${{ runner.os }}-${{ steps.keys.outputs.zigbuild }}-${{ steps.keys.outputs.cargo }} | |
| 73 | + | restore-keys: cli-release-${{ runner.os }}-${{ steps.keys.outputs.zigbuild }}- | |
| 45 | 74 | - name: Build | |
| 46 | 75 | run: node scripts/cli-release.mjs build | |
| 47 | 76 | - uses: actions/upload-artifact@v4 | |
| 189 | 189 | - **One set of paths per entry.** An entry's version is the hash of its | |
| 190 | 190 | paths, so every step that names it, in every workflow, names the same | |
| 191 | 191 | ones. `crates/actions/tests/repository_workflows.rs` holds all of this. | |
| 192 | + | - **sccache only reads on a tag** (`scripts/sccache.sh` sets | |
| 193 | + | `SCCACHE_GHA_RW_MODE=READ_ONLY` when `GITHUB_REF_TYPE` is `tag`): a | |
| 194 | + | release's run finds what `main`'s runs compiled, and saves nothing only | |
| 195 | + | its own tag could read. | |
| 196 | + | - **Images a build runs in are pinned by digest.** The CLI's release | |
| 197 | + | builds in `cargo-zigbuild` (`BUILDER` in `scripts/cli-release.mjs`), and | |
| 198 | + | its cache is keyed by that digest (`zigbuild` from `ci-cache.mjs keys`) | |
| 199 | + | rather than by this machine's `rustc`. | |
| 192 | 200 | ||
| 193 | 201 | | Entry | Key | Saved by | Restored by | | |
| 194 | 202 | | --- | --- | --- | --- | | |
| ⋯ | |||
| 203 | 211 | | apt for Tauri | `apt-tauri-<os>-<scripts/apt-install.sh>` | CI desktop | the desktop release | | |
| 204 | 212 | | The runner release | `runner-release-<os>-<target>-<rust>-<deps>`, zig | Caches (`runner`) | the runner release | | |
| 205 | 213 | | The desktop release | `desktop-release-linux-…`, `desktop-release-windows-…`, `xwin-…`, `apt-tauri-windows-…` | Caches (`desktop-linux`, `desktop-windows`) | the desktop release | | |
| 214 | + | | The CLI release | `cli-release-<os>-<zigbuild>-<deps>` (`target/zig`, every platform) | Caches (`cli`), built whole | the CLI release | | |
| 206 | 215 | ||
| 207 | 216 | To see whether a change helped, read a run's cache steps: `Cache restored | |
| 208 | 217 | from key` names the entry (the key itself is a hit; another is a start), | |
| 209 | 218 | `Cache Size` and the step's time say what it cost, and the post step says | |
| 210 | 219 | whether it saved. `sccache's hits and misses` is on each Rust job's summary. | |
| 220 | + | `g1t cache usage` says what the repository's entries hold against its | |
| 221 | + | 10 GiB, and `g1t cache list --sort size-in-bytes` which take the most. | |
| 211 | 222 | ||
| 212 | 223 | ## Speed | |
| 213 | 224 | ||
| 1 | + | //! A repository's `actions/cache` entries over REST and MCP, in GitHub's | |
| 2 | + | //! shapes: listing them, what they hold together against the repository's | |
| 3 | + | //! quota, and deleting them by key or by id. | |
| 4 | + | //! | |
| 5 | + | //! The actions service keeps them and decides who may see and delete them | |
| 6 | + | //! (services/actions/src/cache.rs); their bytes are in R2, which jobs reach | |
| 7 | + | //! through `/actions/jobs/{job}/cache` and the toolkit's protocols, never | |
| 8 | + | //! through these. | |
| 9 | + | ||
| 10 | + | use g1t_contracts::actions::{CacheList, CacheUsage, CacheUsageArgs, CachesArgs, DeleteCachesArgs}; | |
| 11 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 12 | + | use serde_json::{Value, json}; | |
| 13 | + | use worker::Result; | |
| 14 | + | ||
| 15 | + | use crate::operations::{Services, repo_path}; | |
| 16 | + | ||
| 17 | + | /// One operation on the cache. | |
| 18 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 19 | + | pub enum CachesOp { | |
| 20 | + | ListCaches, | |
| 21 | + | GetCacheUsage, | |
| 22 | + | DeleteCachesByKey, | |
| 23 | + | DeleteCache, | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | impl CachesOp { | |
| 27 | + | /// Every one: `Op::ALL` lists each as `Op::Caches(…)`, which a test | |
| 28 | + | /// checks against this. | |
| 29 | + | #[cfg(test)] | |
| 30 | + | pub const ALL: [CachesOp; 4] = [CachesOp::ListCaches, CachesOp::GetCacheUsage, CachesOp::DeleteCachesByKey, CachesOp::DeleteCache]; | |
| 31 | + | ||
| 32 | + | pub fn name(self) -> &'static str { | |
| 33 | + | match self { | |
| 34 | + | CachesOp::ListCaches => "list_caches", | |
| 35 | + | CachesOp::GetCacheUsage => "get_cache_usage", | |
| 36 | + | CachesOp::DeleteCachesByKey => "delete_caches_by_key", | |
| 37 | + | CachesOp::DeleteCache => "delete_cache", | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | /// For the API reference. | |
| 42 | + | pub fn title(self) -> &'static str { | |
| 43 | + | match self { | |
| 44 | + | CachesOp::ListCaches => "List a repository's caches", | |
| 45 | + | CachesOp::GetCacheUsage => "Get a repository's cache usage", | |
| 46 | + | CachesOp::DeleteCachesByKey => "Delete caches by key", | |
| 47 | + | CachesOp::DeleteCache => "Delete a cache by id", | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | pub fn description(self) -> &'static str { | |
| 52 | + | match self { | |
| 53 | + | CachesOp::ListCaches => "List the entries actions/cache has saved in a repository (total_count, actions_caches): each with its id (a number), key, ref (the ref whose run saved it; a pull request from outside saves under untrusted:<ref>), version (the hash of its paths; empty for one g1t's own actions/cache saved), size_in_bytes, created_at and last_accessed_at (its last restore or lookup). Narrow with key (entries whose key starts with it) and ref; order with sort (last_accessed_at, created_at or size_in_bytes; last_accessed_at by default) and direction (asc or desc; desc by default); page with page and per_page (30 by default, at most 100). Needs the Read role.", | |
| 54 | + | CachesOp::GetCacheUsage => "What a repository's saved cache entries hold together: active_caches_size_in_bytes and active_caches_count, and quota_bytes, the most they may hold (10 GiB). Saving past it evicts the entries restored longest ago. Needs the Read role.", | |
| 55 | + | CachesOp::DeleteCachesByKey => "Delete every cache entry whose key is exactly key, or only ref's when you give ref. Answers with the entries deleted, in list_caches' shape. Their bytes go at once, and a run that restores the key finds nothing until one saves it again. Needs the Write role.", | |
| 56 | + | CachesOp::DeleteCache => "Delete one cache entry by its id, as list_caches gives it. Over REST it answers 204 with no body, as GitHub does. Needs the Write role.", | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | /// Whether it changes anything: what its scope says, which a test holds | |
| 61 | + | /// it to. | |
| 62 | + | #[cfg(test)] | |
| 63 | + | pub fn writes(self) -> bool { | |
| 64 | + | matches!(self, CachesOp::DeleteCachesByKey | CachesOp::DeleteCache) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | pub fn input(self) -> Value { | |
| 68 | + | let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 69 | + | let git_ref = json!({ "type": "string", "description": "A full ref, e.g. refs/heads/main or refs/pull/7/merge." }); | |
| 70 | + | let (properties, required): (Value, &[&str]) = match self { | |
| 71 | + | CachesOp::ListCaches => ( | |
| 72 | + | json!({ | |
| 73 | + | "repo": repo, | |
| 74 | + | "key": { "type": "string", "description": "Only entries whose key starts with this." }, | |
| 75 | + | "ref": git_ref, | |
| 76 | + | "sort": { "type": "string", "enum": ["last_accessed_at", "created_at", "size_in_bytes"], "description": "What to order by: last_accessed_at unless you say." }, | |
| 77 | + | "direction": { "type": "string", "enum": ["asc", "desc"], "description": "desc unless you say." }, | |
| 78 | + | "page": { "type": "integer", "description": "The page, from 1." }, | |
| 79 | + | "per_page": { "type": "integer", "description": "Entries a page: 30 unless you say, at most 100." }, | |
| 80 | + | }), | |
| 81 | + | &["repo"], | |
| 82 | + | ), | |
| 83 | + | CachesOp::GetCacheUsage => (json!({ "repo": repo }), &["repo"]), | |
| 84 | + | CachesOp::DeleteCachesByKey => ( | |
| 85 | + | json!({ | |
| 86 | + | "repo": repo, | |
| 87 | + | "key": { "type": "string", "description": "The key, exactly." }, | |
| 88 | + | "ref": git_ref, | |
| 89 | + | }), | |
| 90 | + | &["repo", "key"], | |
| 91 | + | ), | |
| 92 | + | CachesOp::DeleteCache => ( | |
| 93 | + | json!({ "repo": repo, "id": { "type": ["integer", "string"], "description": "The entry's id, a number." } }), | |
| 94 | + | &["repo", "id"], | |
| 95 | + | ), | |
| 96 | + | }; | |
| 97 | + | json!({ "type": "object", "properties": properties, "required": required }) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | /// A number from a path segment, a query value or a JSON number. | |
| 102 | + | fn number(input: &Value, key: &str) -> Option<u64> { | |
| 103 | + | match &input[key] { | |
| 104 | + | Value::Number(n) => n.as_u64(), | |
| 105 | + | Value::String(s) => s.trim().parse().ok(), | |
| 106 | + | _ => None, | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | fn text(input: &Value, key: &str) -> Option<String> { | |
| 111 | + | input[key].as_str().map(str::trim).filter(|t| !t.is_empty()).map(str::to_owned) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | fn shown<T: serde::Serialize>(outcome: Outcome<T>) -> Outcome<Value> { | |
| 115 | + | match outcome { | |
| 116 | + | Outcome::Ok(value) => Outcome::Ok(serde_json::to_value(value).unwrap_or(Value::Null)), | |
| 117 | + | Outcome::Fail(refused) => Outcome::Fail(refused), | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | pub async fn run(op: CachesOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 122 | + | let Some(repo) = repo_path(input) else { | |
| 123 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\".")); | |
| 124 | + | }; | |
| 125 | + | let actions = &services.actions; | |
| 126 | + | Ok(match op { | |
| 127 | + | CachesOp::ListCaches => { | |
| 128 | + | let args = CachesArgs { | |
| 129 | + | repo, | |
| 130 | + | viewer: viewer.clone(), | |
| 131 | + | key: text(input, "key"), | |
| 132 | + | git_ref: text(input, "ref"), | |
| 133 | + | sort: text(input, "sort"), | |
| 134 | + | direction: text(input, "direction"), | |
| 135 | + | page: number(input, "page").map(|n| n.min(u64::from(u32::MAX)) as u32), | |
| 136 | + | per_page: number(input, "per_page").map(|n| n.min(100) as u32), | |
| 137 | + | }; | |
| 138 | + | let found: Outcome<CacheList> = g1t_kit::call(actions, "caches", &args).await?; | |
| 139 | + | shown(found) | |
| 140 | + | } | |
| 141 | + | CachesOp::GetCacheUsage => { | |
| 142 | + | let found: Outcome<CacheUsage> = g1t_kit::call(actions, "cache_usage", &CacheUsageArgs { repo: repo.clone(), viewer: viewer.clone() }).await?; | |
| 143 | + | match found { | |
| 144 | + | Outcome::Ok(usage) => Outcome::Ok(json!({ | |
| 145 | + | "full_name": format!("{}/{}", repo.namespace, repo.name), | |
| 146 | + | "active_caches_size_in_bytes": usage.active_caches_size_in_bytes, | |
| 147 | + | "active_caches_count": usage.active_caches_count, | |
| 148 | + | "quota_bytes": usage.quota_bytes, | |
| 149 | + | })), | |
| 150 | + | Outcome::Fail(refused) => Outcome::Fail(refused), | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | CachesOp::DeleteCachesByKey | CachesOp::DeleteCache => { | |
| 154 | + | let Some(actor) = viewer.clone() else { | |
| 155 | + | return Ok(Outcome::fail(FailureCode::Unauthenticated, "Deleting cache entries needs a g1t access token.")); | |
| 156 | + | }; | |
| 157 | + | let args = if op == CachesOp::DeleteCache { | |
| 158 | + | let Some(id) = number(input, "id") else { | |
| 159 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Name the cache entry by its id, a number.")); | |
| 160 | + | }; | |
| 161 | + | DeleteCachesArgs { actor, repo, id: Some(id), key: None, git_ref: None } | |
| 162 | + | } else { | |
| 163 | + | let Some(key) = text(input, "key") else { | |
| 164 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the key of the entries to delete.")); | |
| 165 | + | }; | |
| 166 | + | DeleteCachesArgs { actor, repo, id: None, key: Some(key), git_ref: text(input, "ref") } | |
| 167 | + | }; | |
| 168 | + | let done: Outcome<CacheList> = g1t_kit::call(actions, "delete_caches", &args).await?; | |
| 169 | + | shown(done) | |
| 170 | + | } | |
| 171 | + | }) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | #[cfg(test)] | |
| 175 | + | mod tests { | |
| 176 | + | use super::*; | |
| 177 | + | use g1t_contracts::actions::CacheEntry; | |
| 178 | + | ||
| 179 | + | #[test] | |
| 180 | + | fn a_list_is_in_github_s_shape() { | |
| 181 | + | let list = CacheList { | |
| 182 | + | total_count: 1, | |
| 183 | + | actions_caches: vec![CacheEntry { | |
| 184 | + | id: 7, | |
| 185 | + | key: "npm-full-Linux-abc".into(), | |
| 186 | + | git_ref: "refs/heads/main".into(), | |
| 187 | + | version: String::new(), | |
| 188 | + | size_in_bytes: 1024, | |
| 189 | + | created_at: "2026-10-12T10:00:00.000Z".into(), | |
| 190 | + | last_accessed_at: "2026-10-12T11:00:00.000Z".into(), | |
| 191 | + | }], | |
| 192 | + | }; | |
| 193 | + | let Outcome::Ok(sent) = shown(Outcome::Ok(list)) else { panic!() }; | |
| 194 | + | assert_eq!(sent["actions_caches"][0]["ref"], "refs/heads/main"); | |
| 195 | + | assert_eq!(sent["actions_caches"][0]["id"], 7); | |
| 196 | + | assert!(g1t_kit::wire::camel_case_keys(&sent).is_empty()); | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | #[test] | |
| 200 | + | fn each_operation_is_described_with_a_schema() { | |
| 201 | + | for op in CachesOp::ALL { | |
| 202 | + | assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Caches(op)), "{}", op.name()); | |
| 203 | + | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); | |
| 204 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 205 | + | let level = g1t_contracts::scopes::scope_for(op.name()).unwrap().level(); | |
| 206 | + | assert_eq!(op.writes(), level == g1t_contracts::scopes::Level::Write, "{}", op.name()); | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | } |
| 6 | 6 | ||
| 7 | 7 | mod about; | |
| 8 | 8 | mod run_artifacts; | |
| 9 | + | mod caches; | |
| 9 | 10 | mod archives; | |
| 10 | 11 | mod addresses; | |
| 11 | 12 | mod alerts; |
| 9 | 9 | ||
| 10 | 10 | use crate::about::AboutOp; | |
| 11 | 11 | use crate::run_artifacts::ArtifactsOp; | |
| 12 | + | use crate::caches::CachesOp; | |
| 12 | 13 | use crate::archives::ArchiveOp; | |
| 13 | 14 | use crate::deploy_keys::DeployKeysOp; | |
| 14 | 15 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 414 | 415 | ), | |
| 415 | 416 | ( | |
| 416 | 417 | "Actions", | |
| 417 | − | "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.", | |
| 418 | + | "GitHub Actions workflows in .g1t/workflows, their runs, their jobs' logs, their artifacts, and the repository's actions/cache entries.", | |
| 418 | 419 | &[ | |
| 419 | 420 | Op::ListWorkflows, | |
| 420 | 421 | Op::ListWorkflowRuns, | |
| ⋯ | |||
| 431 | 432 | Op::Artifacts(ArtifactsOp::DeleteArtifact), | |
| 432 | 433 | Op::Artifacts(ArtifactsOp::GetArtifactRetention), | |
| 433 | 434 | Op::Artifacts(ArtifactsOp::SetArtifactRetention), | |
| 435 | + | Op::Caches(CachesOp::ListCaches), | |
| 436 | + | Op::Caches(CachesOp::GetCacheUsage), | |
| 437 | + | Op::Caches(CachesOp::DeleteCachesByKey), | |
| 438 | + | Op::Caches(CachesOp::DeleteCache), | |
| 434 | 439 | ], | |
| 435 | 440 | ), | |
| 436 | 441 | ( | |
| ⋯ | |||
| 797 | 802 | Op::Protection(op) => op.title(), | |
| 798 | 803 | Op::Tokens(op) => op.title(), | |
| 799 | 804 | Op::Artifacts(op) => op.title(), | |
| 805 | + | Op::Caches(op) => op.title(), | |
| 800 | 806 | Op::Archives(op) => op.title(), | |
| 801 | 807 | Op::DeployKeys(op) => op.title(), | |
| 802 | 808 | Op::Packages(op) => op.title(), | |
| 29 | 29 | use crate::checks::ChecksOp; | |
| 30 | 30 | use crate::about::AboutOp; | |
| 31 | 31 | use crate::run_artifacts::ArtifactsOp; | |
| 32 | + | use crate::caches::CachesOp; | |
| 32 | 33 | use crate::archives::ArchiveOp; | |
| 33 | 34 | use crate::deploy_keys::DeployKeysOp; | |
| 34 | 35 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 311 | 312 | Tokens(TokenOp), | |
| 312 | 313 | /// Workflow run artifacts, and how long they are kept: run_artifacts.rs. | |
| 313 | 314 | Artifacts(ArtifactsOp), | |
| 315 | + | /// A repository's actions/cache entries: caches.rs. | |
| 316 | + | Caches(CachesOp), | |
| 314 | 317 | /// A branch, tag or commit as a tarball or zipball: archives.rs. | |
| 315 | 318 | Archives(ArchiveOp), | |
| 316 | 319 | /// A repository's deploy keys: deploy_keys.rs. | |
| ⋯ | |||
| 693 | 696 | } | |
| 694 | 697 | ||
| 695 | 698 | impl Op { | |
| 696 | − | pub const ALL: [Op; 367] = [ | |
| 699 | + | pub const ALL: [Op; 371] = [ | |
| 697 | 700 | Op::Whoami, | |
| 698 | 701 | Op::GetWorkspace, | |
| 699 | 702 | Op::CreateWorkspace, | |
| ⋯ | |||
| 986 | 989 | Op::Artifacts(ArtifactsOp::DeleteArtifact), | |
| 987 | 990 | Op::Artifacts(ArtifactsOp::GetArtifactRetention), | |
| 988 | 991 | Op::Artifacts(ArtifactsOp::SetArtifactRetention), | |
| 992 | + | Op::Caches(CachesOp::ListCaches), | |
| 993 | + | Op::Caches(CachesOp::GetCacheUsage), | |
| 994 | + | Op::Caches(CachesOp::DeleteCachesByKey), | |
| 995 | + | Op::Caches(CachesOp::DeleteCache), | |
| 989 | 996 | Op::Archives(ArchiveOp::DownloadTarball), | |
| 990 | 997 | Op::Archives(ArchiveOp::DownloadZipball), | |
| 991 | 998 | Op::DeployKeys(DeployKeysOp::ListDeployKeys), | |
| ⋯ | |||
| 1258 | 1265 | Op::Protection(op) => op.name(), | |
| 1259 | 1266 | Op::Tokens(op) => op.name(), | |
| 1260 | 1267 | Op::Artifacts(op) => op.name(), | |
| 1268 | + | Op::Caches(op) => op.name(), | |
| 1261 | 1269 | Op::Archives(op) => op.name(), | |
| 1262 | 1270 | Op::DeployKeys(op) => op.name(), | |
| 1263 | 1271 | Op::Packages(op) => op.name(), | |
| ⋯ | |||
| 1778 | 1786 | Op::Protection(op) => op.description(), | |
| 1779 | 1787 | Op::Tokens(op) => op.description(), | |
| 1780 | 1788 | Op::Artifacts(op) => op.description(), | |
| 1789 | + | Op::Caches(op) => op.description(), | |
| 1781 | 1790 | Op::Archives(op) => op.description(), | |
| 1782 | 1791 | Op::DeployKeys(op) => op.description(), | |
| 1783 | 1792 | Op::Packages(op) => op.description(), | |
| ⋯ | |||
| 3155 | 3164 | Op::Protection(op) => op.input(), | |
| 3156 | 3165 | Op::Tokens(op) => op.input(), | |
| 3157 | 3166 | Op::Artifacts(op) => op.input(), | |
| 3167 | + | Op::Caches(op) => op.input(), | |
| 3158 | 3168 | Op::Archives(op) => op.input(), | |
| 3159 | 3169 | Op::DeployKeys(op) => op.input(), | |
| 3160 | 3170 | Op::Packages(op) => op.input(), | |
| ⋯ | |||
| 5386 | 5396 | Op::Protection(op) => crate::protection::run(op, services, viewer, input).await, | |
| 5387 | 5397 | Op::Tokens(op) => crate::token_policy::run(op, services, viewer, input).await, | |
| 5388 | 5398 | Op::Artifacts(op) => crate::run_artifacts::run(op, services, viewer, input).await, | |
| 5399 | + | Op::Caches(op) => crate::caches::run(op, services, viewer, input).await, | |
| 5389 | 5400 | Op::Archives(op) => crate::archives::run(op, services, viewer, input).await, | |
| 5390 | 5401 | Op::DeployKeys(op) => crate::deploy_keys::run(op, services, viewer, input).await, | |
| 5391 | 5402 | Op::Packages(op) => crate::packages::run(op, services, viewer, input).await, | |
| 12124 | 12124 | }, | |
| 12125 | 12125 | "notes": "Only artifacts uploaded afterwards are kept the new number of days. A workflow's `retention-days` asks for fewer, never more." | |
| 12126 | 12126 | }, | |
| 12127 | + | "list_caches": { | |
| 12128 | + | "params": { | |
| 12129 | + | "owner": "flagon-io", | |
| 12130 | + | "name": "g1t" | |
| 12131 | + | }, | |
| 12132 | + | "query": { | |
| 12133 | + | "key": "cargo-test-", | |
| 12134 | + | "sort": "size_in_bytes", | |
| 12135 | + | "per_page": "2" | |
| 12136 | + | }, | |
| 12137 | + | "response": { | |
| 12138 | + | "total_count": 3, | |
| 12139 | + | "actions_caches": [ | |
| 12140 | + | { | |
| 12141 | + | "id": 9184, | |
| 12142 | + | "key": "cargo-test-Linux-rustc-1.99.0-5f1c3a9e7b2d", | |
| 12143 | + | "ref": "refs/heads/main", | |
| 12144 | + | "version": "", | |
| 12145 | + | "size_in_bytes": 1687203840, | |
| 12146 | + | "created_at": "2026-10-12T09:14:02.511Z", | |
| 12147 | + | "last_accessed_at": "2026-10-12T15:40:19.087Z" | |
| 12148 | + | }, | |
| 12149 | + | { | |
| 12150 | + | "id": 9102, | |
| 12151 | + | "key": "cargo-test-Linux-rustc-1.99.0-5f1c3a9e7b2d", | |
| 12152 | + | "ref": "refs/pull/412/merge", | |
| 12153 | + | "version": "", | |
| 12154 | + | "size_in_bytes": 702545920, | |
| 12155 | + | "created_at": "2026-10-12T11:02:44.930Z", | |
| 12156 | + | "last_accessed_at": "2026-10-12T11:02:44.930Z" | |
| 12157 | + | } | |
| 12158 | + | ] | |
| 12159 | + | }, | |
| 12160 | + | "notes": "`key` matches keys that start with it. Entries past their 28 days, or unused for 7, are swept within the hour and are not listed once they are." | |
| 12161 | + | }, | |
| 12162 | + | "get_cache_usage": { | |
| 12163 | + | "params": { | |
| 12164 | + | "owner": "flagon-io", | |
| 12165 | + | "name": "g1t" | |
| 12166 | + | }, | |
| 12167 | + | "response": { | |
| 12168 | + | "full_name": "flagon-io/g1t", | |
| 12169 | + | "active_caches_size_in_bytes": 8813275136, | |
| 12170 | + | "active_caches_count": 41, | |
| 12171 | + | "quota_bytes": 10737418240 | |
| 12172 | + | }, | |
| 12173 | + | "notes": "Saving past `quota_bytes` evicts the entries restored longest ago." | |
| 12174 | + | }, | |
| 12175 | + | "delete_caches_by_key": { | |
| 12176 | + | "params": { | |
| 12177 | + | "owner": "flagon-io", | |
| 12178 | + | "name": "g1t" | |
| 12179 | + | }, | |
| 12180 | + | "query": { | |
| 12181 | + | "key": "cargo-test-Linux-rustc-1.99.0-5f1c3a9e7b2d", | |
| 12182 | + | "ref": "refs/pull/412/merge" | |
| 12183 | + | }, | |
| 12184 | + | "response": { | |
| 12185 | + | "total_count": 1, | |
| 12186 | + | "actions_caches": [ | |
| 12187 | + | { | |
| 12188 | + | "id": 9102, | |
| 12189 | + | "key": "cargo-test-Linux-rustc-1.99.0-5f1c3a9e7b2d", | |
| 12190 | + | "ref": "refs/pull/412/merge", | |
| 12191 | + | "version": "", | |
| 12192 | + | "size_in_bytes": 702545920, | |
| 12193 | + | "created_at": "2026-10-12T11:02:44.930Z", | |
| 12194 | + | "last_accessed_at": "2026-10-12T11:02:44.930Z" | |
| 12195 | + | } | |
| 12196 | + | ] | |
| 12197 | + | }, | |
| 12198 | + | "notes": "The key is matched exactly. Without `ref`, the key's entries on every ref are deleted." | |
| 12199 | + | }, | |
| 12200 | + | "delete_cache": { | |
| 12201 | + | "params": { | |
| 12202 | + | "owner": "flagon-io", | |
| 12203 | + | "name": "g1t", | |
| 12204 | + | "id": "9102" | |
| 12205 | + | }, | |
| 12206 | + | "notes": "Answers `204` with no body." | |
| 12207 | + | }, | |
| 12127 | 12208 | "list_packages": { | |
| 12128 | 12209 | "params": { | |
| 12129 | 12210 | "workspace": "acme" |
| 122 | 122 | Op::Telemetry(_) => return as_is, | |
| 123 | 123 | // Artifacts are shaped by the API itself, in `snake_case`. | |
| 124 | 124 | Op::Artifacts(_) => return as_is, | |
| 125 | + | // The cache's entries travel in `snake_case` from the actions service. | |
| 126 | + | Op::Caches(_) => return as_is, | |
| 125 | 127 | // So are archive links. | |
| 126 | 128 | Op::Archives(_) => return as_is, | |
| 127 | 129 | // Deploy keys travel in `snake_case` from identity. |
| 4 | 4 | ||
| 5 | 5 | use crate::about::AboutOp; | |
| 6 | 6 | use crate::run_artifacts::ArtifactsOp; | |
| 7 | + | use crate::caches::CachesOp; | |
| 7 | 8 | use crate::archives::ArchiveOp; | |
| 8 | 9 | use crate::deploy_keys::DeployKeysOp; | |
| 9 | 10 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 861 | 862 | route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]), | |
| 862 | 863 | route( | |
| 863 | 864 | "GET", | |
| 865 | + | "/repos/:owner/:name/actions/caches", | |
| 866 | + | Op::Caches(CachesOp::ListCaches), | |
| 867 | + | &[("key", "key"), ("ref", "ref"), ("sort", "sort"), ("direction", "direction"), ("page", "page"), ("per_page", "per_page")], | |
| 868 | + | ), | |
| 869 | + | route("GET", "/repos/:owner/:name/actions/cache/usage", Op::Caches(CachesOp::GetCacheUsage), &[]), | |
| 870 | + | route("DELETE", "/repos/:owner/:name/actions/caches", Op::Caches(CachesOp::DeleteCachesByKey), &[("key", "key"), ("ref", "ref")]), | |
| 871 | + | route("DELETE", "/repos/:owner/:name/actions/caches/:id", Op::Caches(CachesOp::DeleteCache), &[]), | |
| 872 | + | route( | |
| 873 | + | "GET", | |
| 864 | 874 | "/repos/:owner/:name/actions/secrets", | |
| 865 | 875 | Op::ListActionsSecrets, | |
| 866 | 876 | &[], | |
| ⋯ | |||
| 1079 | 1089 | pub fn no_content(&self) -> bool { | |
| 1080 | 1090 | matches!( | |
| 1081 | 1091 | self.op, | |
| 1082 | − | Op::DeleteComment | |
| 1092 | + | Op::DeleteComment | Op::Caches(CachesOp::DeleteCache) | |
| 1083 | 1093 | ) | |
| 1084 | 1094 | } | |
| 1085 | 1095 | ||
| ⋯ | |||
| 1538 | 1548 | } | |
| 1539 | 1549 | ||
| 1540 | 1550 | #[test] | |
| 1551 | + | fn caches_are_at_githubs_addresses() { | |
| 1552 | + | let query = [("key".to_owned(), "cargo-".to_owned()), ("ref".to_owned(), "refs/heads/main".to_owned()), ("sort".to_owned(), "size_in_bytes".to_owned())]; | |
| 1553 | + | let (route, input) = resolve("GET", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap(); | |
| 1554 | + | assert_eq!(route.op, Op::Caches(CachesOp::ListCaches)); | |
| 1555 | + | assert_eq!(input, json!({ "key": "cargo-", "ref": "refs/heads/main", "sort": "size_in_bytes", "repo": "acme/web" })); | |
| 1556 | + | let (route, _) = resolve("GET", "/repos/acme/web/actions/cache/usage", &[], Value::Null).unwrap(); | |
| 1557 | + | assert_eq!(route.op, Op::Caches(CachesOp::GetCacheUsage)); | |
| 1558 | + | // Deleting by key takes it from the query, as GitHub's does. | |
| 1559 | + | let query = [("key".to_owned(), "npm-full-Linux-abc".to_owned())]; | |
| 1560 | + | let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap(); | |
| 1561 | + | assert_eq!(route.op, Op::Caches(CachesOp::DeleteCachesByKey)); | |
| 1562 | + | assert_eq!(input, json!({ "key": "npm-full-Linux-abc", "repo": "acme/web" })); | |
| 1563 | + | assert!(!route.no_content()); | |
| 1564 | + | let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches/42", &[], Value::Null).unwrap(); | |
| 1565 | + | assert_eq!(route.op, Op::Caches(CachesOp::DeleteCache)); | |
| 1566 | + | assert_eq!(input, json!({ "id": "42", "repo": "acme/web" })); | |
| 1567 | + | assert!(route.no_content()); | |
| 1568 | + | // Only someone signed in sees a repository's cache keys. | |
| 1569 | + | assert!(Op::Caches(CachesOp::ListCaches).needs_user()); | |
| 1570 | + | } | |
| 1571 | + | ||
| 1572 | + | #[test] | |
| 1541 | 1573 | fn query_parameters_are_renamed() { | |
| 1542 | 1574 | let query = [ | |
| 1543 | 1575 | ("q".to_owned(), "parser".to_owned()), | |
| 20 | 20 | ||
| 21 | 21 | use crate::about::AboutOp; | |
| 22 | 22 | use crate::run_artifacts::ArtifactsOp; | |
| 23 | + | use crate::caches::CachesOp; | |
| 23 | 24 | use crate::archives::ArchiveOp; | |
| 24 | 25 | use crate::deploy_keys::DeployKeysOp; | |
| 25 | 26 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 213 | 214 | Tool { | |
| 214 | 215 | name: "workflow", | |
| 215 | 216 | title: "Workflows", | |
| 216 | − | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions, repository dispatch, and the workflows a workspace runs in its repositories without a file in them. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), and their groups.", | |
| 217 | + | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. The repository's actions/cache entries: listing them, what they hold against its 10 GiB, and deleting them. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions, repository dispatch, and the workflows a workspace runs in its repositories without a file in them. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), and their groups.", | |
| 217 | 218 | default_action: None, | |
| 218 | 219 | actions: &[ | |
| 219 | 220 | a("list", Op::ListWorkflows, "Workflows on the default branch"), | |
| ⋯ | |||
| 231 | 232 | a("delete_artifact", Op::Artifacts(ArtifactsOp::DeleteArtifact), "Delete an artifact before it expires"), | |
| 232 | 233 | a("artifact_retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), "Days the repository keeps artifacts"), | |
| 233 | 234 | a("set_artifact_retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), "Change the days the repository keeps artifacts"), | |
| 235 | + | a("list_caches", Op::Caches(CachesOp::ListCaches), "The repository's actions/cache entries: by key prefix or ref, largest or least used first"), | |
| 236 | + | a("cache_usage", Op::Caches(CachesOp::GetCacheUsage), "What the repository's cache entries hold, against its 10 GiB"), | |
| 237 | + | a("delete_caches", Op::Caches(CachesOp::DeleteCachesByKey), "Delete the cache entries with a key, on every ref or one"), | |
| 238 | + | a("delete_cache", Op::Caches(CachesOp::DeleteCache), "Delete one cache entry by its id"), | |
| 234 | 239 | a("combined_status", Op::Checks(ChecksOp::GetCombinedStatus), "A commit's statuses and the state they add up to"), | |
| 235 | 240 | a("list_statuses", Op::Checks(ChecksOp::ListCommitStatuses), "A commit's statuses, newest first"), | |
| 236 | 241 | a("set_status", Op::Checks(ChecksOp::CreateCommitStatus), "Set a status on a commit"), | |
| 825 | 825 | at R2's price plus the margin; see | |
| 826 | 826 | [usage and billing](/guides/usage-and-billing/#actions-cache). | |
| 827 | 827 | ||
| 828 | + | ### Seeing and deleting entries | |
| 829 | + | ||
| 830 | + | To see what a repository's cache holds against its 10 GiB, and which | |
| 831 | + | entries take the most: | |
| 832 | + | ||
| 833 | + | ```sh | |
| 834 | + | g1t cache usage | |
| 835 | + | g1t cache list --sort size-in-bytes --limit 10 | |
| 836 | + | g1t cache list --key cargo- --ref refs/heads/main | |
| 837 | + | ``` | |
| 838 | + | ||
| 839 | + | Each entry shows its id, key, the ref whose run saved it (a pull request | |
| 840 | + | from outside the repository saves under `untrusted:<ref>`), its size, and | |
| 841 | + | when it was saved and last restored. To delete one, or every entry with a | |
| 842 | + | key (on one ref with `--ref`): | |
| 843 | + | ||
| 844 | + | ```sh | |
| 845 | + | g1t cache delete 9184 | |
| 846 | + | g1t cache delete cargo-test-Linux-1.99.0-5f1c3a9e7b2d --ref refs/pull/412/merge | |
| 847 | + | ``` | |
| 848 | + | ||
| 849 | + | A deleted entry's bytes go at once, and a run that restores its key finds | |
| 850 | + | nothing until one saves it again. Listing needs the Read role, deleting | |
| 851 | + | the Write role. | |
| 852 | + | ||
| 853 | + | | `workflow` action | Route | | |
| 854 | + | | --- | --- | | |
| 855 | + | | `list_caches` | `GET /repos/{owner}/{repo}/actions/caches`, with `key` (a prefix), `ref`, `sort` (`last_accessed_at`, `created_at` or `size_in_bytes`), `direction`, `page`, `per_page` | | |
| 856 | + | | `cache_usage` | `GET /repos/{owner}/{repo}/actions/cache/usage`: `active_caches_size_in_bytes`, `active_caches_count`, and `quota_bytes` | | |
| 857 | + | | `delete_caches` | `DELETE /repos/{owner}/{repo}/actions/caches?key=…`, with `ref`: answers with the entries deleted | | |
| 858 | + | | `delete_cache` | `DELETE /repos/{owner}/{repo}/actions/caches/{id}`: `204` | | |
| 859 | + | ||
| 828 | 860 | ## Artifacts | |
| 829 | 861 | ||
| 830 | 862 | `actions/upload-artifact` keeps files a job made with its run, for later | |
| ⋯ | |||
| 1022 | 1054 | | What is compiled each time | What rustc links: binaries, `cdylib` crates, proc macros, build scripts and test harnesses. | | |
| 1023 | 1055 | | Its entries | One per compiled crate, often thousands for a workspace, each a few KB to a few MB. They count toward the repository's 10 GiB like any other entry, and the ones not restored for 7 days are deleted. | | |
| 1024 | 1056 | | Branches | A pull request reads what the default branch saved. Run the workflow on pushes to the default branch too, as above, or each pull request's first run starts with an empty cache. | | |
| 1057 | + | | Releases from tags | A tag's run reads the default branch's entries, but what it saves only that tag would ever read. Set `SCCACHE_GHA_RW_MODE: READ_ONLY` on a tag's run (`if [ "$GITHUB_REF_TYPE" = tag ]`) so it saves nothing: it still looks every crate up. | | |
| 1025 | 1058 | | Starting over | Set `SCCACHE_GHA_VERSION` to any new value. A new sccache release starts over by itself. | | |
| 1026 | 1059 | | If the cache cannot be reached | sccache refuses to start. Set `SCCACHE_IGNORE_SERVER_IO_ERROR: "1"` to have rustc run without it instead. | | |
| 1027 | 1060 | ||
| ⋯ | |||
| 2012 | 2045 | | `download_artifact` | `GET …/actions/artifacts/{artifact_id}/zip`: a `302` to a link good for 10 minutes | | |
| 2013 | 2046 | | `delete_artifact` | `DELETE …/actions/artifacts/{artifact_id}` | | |
| 2014 | 2047 | | `artifact_retention`, `set_artifact_retention` | `GET` and `PUT …/actions/permissions/artifact-and-log-retention` with `days` (it sets artifacts' days only; logs are kept for 90 days) | | |
| 2048 | + | | `list_caches`, `cache_usage`, `delete_caches`, `delete_cache` | `…/actions/caches` and `…/actions/cache/usage`; see [Seeing and deleting entries](#seeing-and-deleting-entries) | | |
| 2015 | 2049 | ||
| 2016 | 2050 | Secrets and variables have a tool of their own, `secret`: | |
| 2017 | 2051 | ||
| 674 | 674 | ||
| 675 | 675 | Calls [`delete_actions_secret`](/reference/api/secrets-and-variables/delete-actions-secret/), [`delete_actions_secret_for_workspace`](/reference/api/secrets-and-variables/delete-actions-secret-for-workspace/). | |
| 676 | 676 | ||
| 677 | + | ## g1t cache | |
| 678 | + | ||
| 679 | + | The repository's actions/cache entries: list, usage, delete. | |
| 680 | + | ||
| 681 | + | ### g1t cache list | |
| 682 | + | ||
| 683 | + | List the repository's cache entries, least recently used last. | |
| 684 | + | ||
| 685 | + | ```text | |
| 686 | + | g1t cache list [OPTIONS] | |
| 687 | + | ``` | |
| 688 | + | ||
| 689 | + | | Argument or flag | | | |
| 690 | + | | --- | --- | | |
| 691 | + | | `-k, --key <KEY>` | Only entries whose key starts with this. | | |
| 692 | + | | `-g, --ref <REF>` | Only entries saved by this ref's runs, e.g. refs/heads/main. | | |
| 693 | + | | `-S, --sort <SORT>` | One of `last-accessed-at`, `created-at`, `size-in-bytes`. Default: `last-accessed-at`. | | |
| 694 | + | | `-O, --order <ORDER>` | One of `asc`, `desc`. Default: `desc`. | | |
| 695 | + | | `-L, --limit <LIMIT>` | Default: `30`. | | |
| 696 | + | ||
| 697 | + | Calls [`list_caches`](/reference/api/actions/list-caches/). | |
| 698 | + | ||
| 699 | + | ### g1t cache usage | |
| 700 | + | ||
| 701 | + | What the repository's cache entries hold, against its quota. | |
| 702 | + | ||
| 703 | + | ```text | |
| 704 | + | g1t cache usage [OPTIONS] | |
| 705 | + | ``` | |
| 706 | + | ||
| 707 | + | Calls [`get_cache_usage`](/reference/api/actions/get-cache-usage/). | |
| 708 | + | ||
| 709 | + | ### g1t cache delete | |
| 710 | + | ||
| 711 | + | Delete cache entries: one by its id, or every entry with a key. | |
| 712 | + | ||
| 713 | + | ```text | |
| 714 | + | g1t cache delete [OPTIONS] <ENTRY> | |
| 715 | + | ``` | |
| 716 | + | ||
| 717 | + | | Argument or flag | | | |
| 718 | + | | --- | --- | | |
| 719 | + | | `<ENTRY>` | An entry's id (a number, from `g1t cache list`), or a key: every entry with exactly that key. | | |
| 720 | + | | `-g, --ref <REF>` | With a key, only the entries this ref's runs saved. | | |
| 721 | + | ||
| 722 | + | Calls [`delete_cache`](/reference/api/actions/delete-cache/), [`delete_caches_by_key`](/reference/api/actions/delete-caches-by-key/). | |
| 723 | + | ||
| 677 | 724 | ## g1t deploy | |
| 678 | 725 | ||
| 679 | 726 | Deployments: list and view. |
| 432 | 432 | | [`delete_artifact`](/reference/api/actions/delete-artifact/) | Delete it before it expires. Write role. | `repo`, `id` | `workflows:write` | | |
| 433 | 433 | | [`artifact_retention`](/reference/api/actions/get-artifact-retention/) | The days the repository keeps artifacts, and the most it may. | `repo` | `workflows:read` | | |
| 434 | 434 | | [`set_artifact_retention`](/reference/api/actions/set-artifact-retention/) | Set those days, 1 to 90. Maintain role. | `repo`, `days` | `workflows:write` | | |
| 435 | + | | [`list_caches`](/reference/api/actions/list-caches/) | The repository's `actions/cache` entries: id, key, ref, size, when saved and last restored; `key` (a prefix), `ref`, `sort` (`last_accessed_at`, `created_at`, `size_in_bytes`), `direction`, `page`, `per_page`. | `repo` | `workflows:read` | | |
| 436 | + | | [`cache_usage`](/reference/api/actions/get-cache-usage/) | What the entries hold together, their count, and the repository's 10 GiB (`quota_bytes`). | `repo` | `workflows:read` | | |
| 437 | + | | [`delete_caches`](/reference/api/actions/delete-caches-by-key/) | Delete the entries whose key is exactly `key`; only `ref`'s with `ref`. Write role. | `repo`, `key` | `workflows:write` | | |
| 438 | + | | [`delete_cache`](/reference/api/actions/delete-cache/) | Delete one entry by its id. Write role. | `repo`, `id` | `workflows:write` | | |
| 435 | 439 | | [`combined_status`](/reference/api/checks/get-combined-status/) | A commit's statuses, one per context, and the `state` they add up to. `ref` is a SHA, branch or tag. | `repo`, `ref` | `checks:read` | | |
| 436 | 440 | | [`list_statuses`](/reference/api/checks/list-commit-statuses/) | A commit's statuses, newest first. | `repo`, `ref` | `checks:read` | | |
| 437 | 441 | | [`set_status`](/reference/api/checks/create-commit-status/) | Set a status: `state`, `context`, `description`, `target_url`. Write role. | `repo`, `sha`, `state` | `checks:write` | |
Binary or large file; its contents are not shown.
| 344 | 344 | "apt-tauri-windows-", | |
| 345 | 345 | "xwin-", | |
| 346 | 346 | "zig-0.13.0-zigbuild-", | |
| 347 | + | "cli-release-", | |
| 347 | 348 | ] { | |
| 348 | 349 | assert!(seen.contains_key(family), "{family}: {:?}", seen.keys()); | |
| 349 | 350 | } | |
| ⋯ | |||
| 403 | 404 | ); | |
| 404 | 405 | let target = paths(&step).iter().any(|p| p.contains("target")); | |
| 405 | 406 | if target { | |
| 406 | − | assert!(key.contains("steps.keys.outputs.rust"), "{file} {}: {key}", job.id); | |
| 407 | + | // The toolchain that built it: this machine's rustc, or for | |
| 408 | + | // the CLI's release, the cargo-zigbuild image it builds in. | |
| 409 | + | let toolchain = if paths(&step).iter().any(|p| p.starts_with("target/zig")) { | |
| 410 | + | "steps.keys.outputs.zigbuild" | |
| 411 | + | } else { | |
| 412 | + | "steps.keys.outputs.rust" | |
| 413 | + | }; | |
| 414 | + | assert!(key.contains(toolchain), "{file} {}: {key}", job.id); | |
| 407 | 415 | // A restore key never reaches another toolchain's entries. | |
| 408 | 416 | for prefix in step.raw["with"]["restore-keys"] | |
| 409 | 417 | .as_str() | |
| ⋯ | |||
| 411 | 419 | .lines() | |
| 412 | 420 | .filter(|l| !l.trim().is_empty()) | |
| 413 | 421 | { | |
| 414 | − | assert!(prefix.contains("steps.keys.outputs.rust"), "{file} {}: {prefix}", job.id); | |
| 422 | + | assert!(prefix.contains(toolchain), "{file} {}: {prefix}", job.id); | |
| 415 | 423 | } | |
| 416 | 424 | } | |
| 417 | 425 | if key.contains("steps.keys.outputs.") { | |
| ⋯ | |||
| 463 | 471 | "{file} {id}" | |
| 464 | 472 | ); | |
| 465 | 473 | } | |
| 474 | + | // The CLI's build is made in the builder image, whose Cargo the host | |
| 475 | + | // cannot ask what it built: so it is never started from an older entry, | |
| 476 | + | // and has nothing old to prune. | |
| 477 | + | let caches = read("caches.yml"); | |
| 478 | + | let cli = caches.jobs.iter().find(|j| j.id == "cli").unwrap(); | |
| 479 | + | let build = cli | |
| 480 | + | .steps | |
| 481 | + | .iter() | |
| 482 | + | .find(|s| s.raw["with"]["key"].as_str().is_some_and(|k| k.starts_with("cli-release-"))) | |
| 483 | + | .unwrap(); | |
| 484 | + | assert!(saves(build)); | |
| 485 | + | assert!(build.raw["with"].get("restore-keys").is_none()); | |
| 466 | 486 | let ci = read("ci.yml"); | |
| 467 | 487 | let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap(); | |
| 468 | 488 | assert_eq!( | |
| ⋯ | |||
| 976 | 996 | assert!(script.contains("sha256sum -c")); | |
| 977 | 997 | assert!(script.contains("RUSTC_WRAPPER=")); | |
| 978 | 998 | assert!(script.contains("GITHUB_STEP_SUMMARY")); | |
| 999 | + | // A tag's run reads main's entries and saves none, which only its own | |
| 1000 | + | // tag could read; the mode reaches the steps after it too. | |
| 1001 | + | assert!(script.contains("if [ \"${GITHUB_REF_TYPE:-}\" = tag ]; then\n export SCCACHE_GHA_RW_MODE=READ_ONLY")); | |
| 1002 | + | assert!(script.contains("echo \"SCCACHE_GHA_RW_MODE=$SCCACHE_GHA_RW_MODE\"")); | |
| 1003 | + | let tagged = read("runner-release.yml"); | |
| 1004 | + | assert!(tagged.trigger("push").unwrap().tags.allows("runner-v0.4.0")); | |
| 979 | 1005 | } | |
| 980 | 1006 | ||
| 981 | 1007 | #[test] | |
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…