CI design
Apollo Download Manager's CI runs on GitHub Actions. Since 2026-10-31 the repository is public, owned by Singlbase Technologies Limited, and GitHub's standard runners are free for public repositories (billing). It was designed for a private repository with a monthly quota of minutes, so every design choice below still keeps runs short: the checks that matter run on every change, and nothing runs twice. The cost figures below are from those private months.
This page is the design. The workflows are written in #13, #14, #16 to #19 and #91. Every figure below is labelled as measured or assumed. Assumed CI times get replaced by measured ones as the jobs start running (#14).
History: CI by hand only (2026-10-24 to 2026-10-31)
To save Actions minutes while the repository was private, CI was paused (2026-10-24, #173), then run only by hand, once per batch of five issues on a batch/ branch, with no tests on the developer's machine (2026-10-30, #204), then not at all while Apollo Download Manager was made ready to publish (2026-10-31). On 2026-10-31 the repository went public, so its minutes are free, and the triggers below came back (#236). The batch runs are cited in some PRs and code comments by their run numbers.
Tiers
| # | Event | What runs | Platforms | Purpose | Built in |
|---|---|---|---|---|---|
| 1 | pull_request into main (opened, synchronised, reopened) | L1 for the selected areas, plus L2 for those areas. Docs-only changes run only the docs job | Linux | Fast feedback before review. One aggregate check, ci-required, summarises it | #13, #14, #91 |
| 2 | push to main (that is, after a squash merge) | Selected like the PR tier, against the commit main was at before the push (main.yml). Linux: the L1 checks the merge needs (check:pr), then save the Rust cache. Windows: every L1 check (check:fast) when Windows-relevant paths changed (below). The docs and guidance checks always. A red run opens the tracking issue (below) | Linux, Windows | Catch what the PR tier deliberately left out, on Windows too, before a release candidate is built from main, and keep the PR caches warm | #14, #16 |
| 3 | workflow_dispatch on a commit, or a v*-rc.* tag | Everything runnable (check:full and the L2 suites) plus installers | Linux, Windows; macOS once #83 establishes it | A downloadable, labelled release-candidate build from a verified main commit | #17 |
| 4 | v* tag (not -rc) | The release gate: it rechecks that the tag's commit passed tier 3, then builds artefacts with provenance into a draft release | Linux, Windows | Prepare a release the PO publishes by hand | #18 |
| 5 | schedule (weekly) and workflow_dispatch | L4: everything on Linux and Windows. Later also fuzzing, stress and extended benchmarks. macOS on demand only | Linux, Windows; macOS manual | Catch slow drift and run what's too expensive per change | #19 |
The website has a workflow of its own, site.yml: it builds the site on pull requests that change its sources, and builds and deploys it on main and when a release is published (WEBSITE.md).
No develop or staging tiers. All work merges into main, and releases are tags on verified main commits (DEVELOPMENT_WORKFLOW.md).
Required checks. The Main ruleset requires a pull request and linear history, and blocks deleting main and force-pushing to it (REPOSITORY_SETTINGS.md). ci-required becomes a required status check once PR checks have run on a pull request since they came back (#236): GitHub only offers a check it has seen. Until then:
"Required" means by convention: the PO merges only when
ci-requiredis green (docs/REVIEWING.md).The release gate is a person: only the PO pushes
v*tags or starts a release run (an ask rule in.claude/settings.jsonstops the agent from doing it unprompted). The workflow creates a draft release, never a published one.Next: the PO adds
ci-requiredas a required status check in the Main ruleset (REPOSITORY_SETTINGS.md).
Selecting jobs
Selection extends the rules pnpm check:pr already uses, in scripts/checks-lib.mjs. The same code decides locally and in CI, so the two can't drift apart (#13).
Fail-safe: these select everything:
lockfiles (
Cargo.lock,pnpm-lock.yaml);native lock files anywhere, even inside a crate (
*.lock,vcpkg.json);shared configuration (
Cargo.toml,package.json,rust-toolchain.toml…);the check scripts (
scripts/),.github/and.claude/;any path no rule knows, such as a
package-lock.json, which Apollo Download Manager doesn't use.
Crates: a change inside a crate selects that crate and every crate that depends on it.
Frontend: frontend paths select
tsc, ESLint and Vitest.Docs only:
docs/and root*.mdselect only the docs job (pnpm check:links: relative links and heading anchors).Native builds (#35): the Windows native job runs only for the native inputs (
scripts/native/,crates/velocity-curl/,.cargo/, and native lock files other thanCargo.lockandpnpm-lock.yaml), for.github/, and for paths no rule knows. On Linux, thecodejob builds the native libraries itself before its Rust checks. Other shared files, such asCargo.lockor another check script, don't select them: the builds take minutes, and Windows minutes bill double (BUILDING_NATIVE.md).Windows-relevant paths (tier 2):
every Rust crate,
src-tauri/,Cargo.lock,rust-toolchain.toml;scripts/, since the check runner has Windows-specific code paths;packaging configuration;
and, as always, unknown paths.
Frontend-only and docs-only merges skip Windows, because a WebView difference is caught by the release-candidate tier instead.
Every selection prints its reasons in the job log, as
check:prdoes locally. A job that wasn't selected is reported as NOT RUN, never as passed.
Saving minutes never removes a relevant correctness or security test. When in doubt, the selector selects more.
How the PR tier applies it
The PR tier is implemented in .github/workflows/pr.yml (#13):
| Job | Runs when | What it does |
|---|---|---|
select | Always | node scripts/ci-select.mjs --base <PR base> writes rust, frontend, docs and native flags to its outputs. It adds a table to the run summary with every job, runs or skipped, the reason for each, and the reason for each changed path. When docs are selected, the same job runs node scripts/check-links.mjs as a step |
select: product-name step | Always | node scripts/check-product-name.mjs (#421, TESTING.md): no V2 for the product in what people read. Under a second, and a source change counts as much as a document, so it isn't selected by path |
select: guidance step | Always | node scripts/check-guidance.mjs (AGENT_GUIDANCE.md). Under a second, and any path can change its result (a deleted folder kills a rule's glob), so it isn't selected by path |
code | Rust or frontend selected | pnpm check:pr, the same command as locally. With Rust, scripts/ci-sftp-server.sh first installs OpenSSH if needed and starts the password-login SFTP server the SFTP tests use (#53). Tauri system packages and the pinned toolchain are installed only when Rust is selected. With Rust, check:pr builds the native libraries first (reused from the target/ cache that main saves), and the job uploads build-info-linux |
native-windows | Native inputs, CI files or unknown paths changed | On windows-latest: node scripts/native/build.mjs (verifies every source, builds, checks curl -V against the lock file), uploads build-info-windows (7 days), then cargo test -p velocity-curl, which links the build and runs the start check and the TLS tests, and then the Windows certificate-store test (--test windows_store -- --ignored), which needs the runner's administrator rights (SECURITY.md, #69). No cache |
ci-required | Unless the run was cancelled (if: ${{ !cancelled() }}) | Passes only if select succeeded, and code and native-windows each succeeded or were skipped by the selection. It writes its own table to the run summary. A run superseded by a newer push is cancelled whole and reports nothing; always() would report a false failure there |
Why so few jobs. GitHub bills every job's time rounded up to a whole minute (runner pricing), and every job repeats checkout and setup. So Rust and frontend share the code job, and the one-second docs check runs inside select. A docs-only PR then costs 2 billed minutes (select and ci-required), and a code PR costs the code job plus 2.
Skipped jobs and required checks. A job skipped by an if: condition reports Success, while a workflow skipped by path filters leaves a required check Pending forever (troubleshooting required checks). So the workflow never uses on.paths, and ci-required reports on every run that isn't cancelled. That keeps it usable as a required check if branch protection becomes available (#7).
Selection errors fail the run. If ci-select.mjs can't compute the change (for example, missing history), it exits non-zero rather than guessing, and ci-required fails.
Caching
The repository gets 10 GB of Actions cache, separate from artifact storage. GitHub evicts the least recently used entries above that (billing).
| Cache | Key | Contents | Size | Reasoning |
|---|---|---|---|---|
| Rust (Linux) | OS + toolchain + Cargo.lock hash | Compiled dependencies only (~/.cargo/registry, target/ without workspace crates) | Measured in CI: 319 MiB as saved (1.1 GiB of target/ plus a 201 MiB registry before compression); locally 365 MiB with gzip | A cold build is most of a cold job (measured: 571 s for the clippy and test builds on 2 vCPUs). Workspace crates rebuild in seconds, so caching them would only add upload time and size |
| Rust (Windows) | Same | Same | Assumed: similar to Linux (not measured) | Windows minutes cost more ($0.010 against $0.006 a minute), so a cache hit saves more |
| Native libraries (#205), per OS | OS + hash of scripts/native/** (the build scripts, the lock file and the vcpkg manifest and ports) | target/native: libcurl and its libraries, and libtorrent, Boost and OpenSSL from vcpkg | Not measured yet | Building Boost and libtorrent from source takes a large part of a job, so they are kept on their own, and saved even after a red run |
| pnpm store | — | Not cached | 147 MiB (Linux) | pnpm install --frozen-lockfile took 4 s in CI (PR #104), less than a cache restore and save would cost (PO decision on #14) |
Caching rules:
Budget: about 0.6 GB a platform when compressed, so Linux and Windows, with one previous key each during a lockfile change, use about 2.5 GB of the 10 GB.
Save on
mainonly; pull requests restore. A PR branch's cache is visible only to that branch, so saving it there would fill the 10 GB with copies.Keys change only when the toolchain or lockfile changes, so a cache uploads rarely.
No cache for Tauri system packages (
apt): they installed in 35 s in CI (PR #104), about what restoring a cache of them would take.
How it's wired (#14):
Action: GitHub's own
actions/cache/restoreandactions/cache/save, pinned by SHA. No third-party cache action is used, because any third-party action is a dependency that needs PO approval (AGENTS.md).Setup: the shared composite action
.github/actions/setuprestores the cache for every job that builds Rust. When the exact key is missing, it falls back to the newest older key (restore-keys), so a dependency bump starts mostly warm.Saving: only
main.yml, the Linux checks after each merge tomain, saves the cache. It does so after a fully green run, when the exact key isn't cached yet, and after removing the workspace crates' artefacts.The native libraries (#205): a composite action of their own,
.github/actions/native, called by the setup action and by PR checks' Windows native job, restorestarget/native. Only when that misses does it set up vcpkg: a clone ofmicrosoft/vcpkgat thebuiltin-baselinecommit ofscripts/native/vcpkg/vcpkg.json, bootstrapped with-disableMetrics, withVCPKG_DISABLE_METRICS=1. Every job that builds them saves them right after its checks, whatever their result, so a rerun on the same branch skips the build. A cold build of Boost and libtorrent is why the Rust jobs may run up to 90 minutes (Windows Main checks 120); a warm one takes far less. GitHub restores a cache only on the branch that saved it or from the default branch, so a new branch builds them once unless a run onmainsaved them.Cache scopes (#82): the Linux packages workflow builds on Ubuntu 22.04, while the checks run on
ubuntu-latest. Libraries built on the newer Ubuntu can need a newer glibc than 22.04 has, so its jobs pass the setup action'scache-scopeinput, and their keys readrust-v1-Linux-ubuntu22.04-…andnative-v1-Linux-ubuntu22.04-…. It saves its native libraries as the Windows installer workflow does, one more native cache in the 10 GB (size not measured yet), and saves no Rust cache (PACKAGING.md).Release candidates use no cache (#233): their packaging runs restore no Rust or native cache and save none (the setup action's
coldinput), so nothing an earlier run saved can reach an installer. CodeQL's cache-poisoning rules flagged the warm build. A candidate takes longer, about 30 to 60 minutes instead of 10 to 20 (an estimate from the cold native build of #205; not measured yet), and candidates are rare (PO decision, 2026-10-31). A packaging run started by hand still uses and saves the caches.No incremental compilation in CI:
CARGO_INCREMENTAL=0keeps the cache smaller and CI builds faster.
Runner disk. A hosted Linux runner has about 14 GB free. A cold build of Boost and libtorrent plus check:fast's debug build filled it twice (#76), so the Linux jobs that build Rust first delete toolchains Apollo Download Manager never uses (.NET, the Android SDK, GHC and CodeQL), about 20 GB.
The post-merge tier
main.yml has four jobs (#16):
| Job | Runs when | What it does |
|---|---|---|
select | Always | node scripts/ci-select.mjs --base <github.event.before>: the same selection as a pull request, for what the merge changed. Also check:links, check:product-name and check:guidance |
linux | Rust or frontend selected | With Rust, the SFTP test servers (scripts/ci-sftp-server.sh, #53). check:pr --base <before>, which runs the native build before any Rust check, then saves the Linux Rust cache when Rust ran and its key is new |
windows | Windows-relevant paths changed (Rust, scripts/, CI files, unknown paths) | The OpenSSH Server feature if the image lacks it (#53), then check:fast on windows-latest, with its own Rust cache (key rust-v1-Windows-<hash>). It doesn't depend on linux, so a Linux-only failure doesn't stop it (scenario B of #91) |
summary | Unless the run was cancelled | Combines the job reports into the run summary and ci-diagnostics, then opens, comments on or closes the tracking issue. It's the only job allowed to write issues |
A docs-only merge runs only select and summary.
Manual runs: gh workflow run main.yml --ref <branch> [-f base=<commit>] [-f tracking-title="<title>"]. They're for trying a change to this workflow. Without base, a manual run checks everything. A tracking title is for test runs only: on main the title is always main is failing, so a test run never touches the real issue.
Packaging and release candidates
Three workflows are started by hand only, and run on no push or pull request, and a fourth by a release tag. They aren't checks:
Windows installer (
package-windows.yml) and Linux packages (package-linux.yml) build and check the packages of the branch they are started on (PACKAGING.md).Release candidate (
release-candidate.yml) verifies amaincommit, calls both packaging workflows for it (workflow_call, with the commit asref), and assembles the candidate (RELEASE_CANDIDATES.md). AvX.Y.Z-rc.Ntag starts it too. It needsactions: read, to find the commit's Main checks run.Release (
release.yml), on avX.Y.Ztag: publishes an accepted candidate's files as a draft release, after the PO's approval in thereleaseenvironment (RELEASE_PROCESS.md). Only itsdraftjob may write (contents: write);assemblesigns a provenance attestation (id-token: write,attestations: write).Strict needs (#91): in these four, a job runs only when every job it needs succeeded. No job condition uses
always(),!cancelled(),failure()orcancelled(), and nothing usescontinue-on-error; tests read the four files (scripts/release-candidate-lib.test.mjs,scripts/release-lib.test.mjs). A step may still save a cache after a failed step, as the native libraries' does.A new one can't be tried on a branch: GitHub starts a hand-started workflow only from a file already on
main, so each is first run after it merges.
When main breaks
main must stay releasable (DEVELOPMENT_WORKFLOW.md):
Visible within the run: a red
mainrun opens the issue "main is failing". It holds the failing checks with their first error, the run link and this policy (scripts/ci-main-status.mjs). While the issue is open, each further red run comments on it, and the next green run closes it.The policy: fix forward with a
fix/PR, or revert the offending squash commit through a PR, within one working day (project judgment). The tracking issue is where that is coordinated.A missing report counts as red: if a run leaves no
ci-diagnosticsreport, the issue still opens, saying so. A broken run is never silent.
Cancelling and de-duplicating
Superseded PR runs are cancelled. Each workflow sets
concurrency: { group: <workflow>-<PR or ref>, cancel-in-progress: true }for pull requests (concurrency). Runs onmainand on tags are never cancelled: each commit's result matters.No duplicate runs for one commit. Pull-request checks trigger on
pull_requestonly, andpushtriggers only formainand tags. A branch push therefore doesn't run CI a second time beside its PR.Path filters live in the selector, not in
on.paths. Then the aggregateci-requiredcheck always reports, instead of being missing when a workflow is skipped.
Permissions and supply chain
GitHub's guidance is in secure use:
Read-only by default. Every workflow declares
permissions: contents: read, and a job raises only what it needs (for examplecontents: writefor the draft-release job in #18).Pinned actions. Third-party actions are pinned to a full commit SHA, with the version in a comment. GitHub's own
actions/*are pinned the same way, for consistency.Dependabot updates the pins:
.github/dependabot.ymlchecks the workflows and the composite action monthly, grouped into one PR, so an update costs one CI run. Each update is an ordinary PR the PO reviews. For Cargo and npm, Dependabot opens security-fix PRs only, no routine version updates (PO decision on #75); other dependency changes need PO approval (#9).No
pull_request_targetthat checks out PR code. No secrets in PR workflows.Untrusted text stays out of commands. Titles, branch names and other user-controlled strings never go straight into
run:scripts; they pass through environment variables.Third-party notices (#76): the Select jobs job of PR checks and Main checks runs
pnpm check:noticeswhenever Rust is selected, which every lockfile change does, so a failure stops the run before the long checks. It fails on a licence outside the allowed list and on aTHIRD_PARTY_NOTICES.mdthat no longer matches the dependency graphs (LICENSING.md). Once, on Linux: the graphs cover every target platform. Dependabot's PRs fail it, because Dependabot doesn't regenerate the file; how they're handled is in SECURITY.md.Self-hosted runners: none. If one is ever added (PO decision), it never runs code from a fork.
Collecting failures
Every run completes every check that can still run, and reports all failures in one place (#91). The details are in CI_DIAGNOSTICS.md.
The quick checks come first (PO decision on #76, 2026-10-30). The docs, product-name, guidance and notices checks run in the select job and take about a minute. When one fails, the Linux and Windows jobs don't start, and the summary reports them BLOCKED by select. The run ends in about a minute instead of 30 to 40, so a broken link costs a minute to find, not a whole run. Fix it and run again; the code checks then run. Until #76, the code jobs ran even after a failed docs check, so that one broken link wouldn't hide every code failure (#91).
Checks run to completion:
The check runner runs every check even after one fails.
Clippy uses
--keep-goingandcargo testuses--no-fail-fast.Report and summary steps use
if: ${{ !cancelled() }}.Mandatory checks never use
continue-on-error, which would turn a failure into a success.Cross-platform matrices (#16, #17, #19) use
fail-fast: false.
Infrastructure problems are reported as INFRA, separately from code defects. That covers a failed checkout, toolchain install, cache restore or
pnpm install, and a job that died before reporting. A check that couldn't run because its prerequisite failed is BLOCKED, with the prerequisite named.One consolidated report.
ci-required(PRs) and themainjob publish a table in the run summary and theci-diagnosticsartifact (ci-diagnostics.json). The run fails unless every mandatory check passed or was deselected. The/ci-triageskill reads it.Gated jobs stay strict. Packaging and release jobs (#17, #18) keep the default
needssemantics, so nothing is built for release after a failed check.
L4 schedule
| Run | When | Content | Platforms |
|---|---|---|---|
| Weekly full | Mondays 03:00 UTC, only if main changed since the last run | check:full, all L2 suites | Linux, Windows |
| Fuzzing, stress, extended benchmarks | Weekly, staggered, once the suites exist (#73, #77) | Time-boxed per target, with a fixed seed recorded | Linux |
| macOS | workflow_dispatch only | check:full | macOS (#83) |
Advisories (advisories.yml, #75) | Mondays 04:00 UTC, and by hand (workflow_dispatch) | pnpm check:advisories: cargo-deny and pnpm audit against the lockfiles (SECURITY.md) | Linux |
macOS minutes cost about 10 times Linux ($0.062 against $0.006 a minute), so macOS never runs on a schedule or per change until it's an established platform and the quota allows it.
Costs
Measured locally (docs/DEVELOPMENT.md, 2026-10-21). A cold dependency build takes 278 s on Windows (16 logical CPUs) and 413 s on Linux (WSL2, 2 vCPUs, similar to a 2-vCPU hosted runner). A warm check:fast takes 5.5–9 s. A full check:full takes 224–249 s, of which 215–232 s is the release build.
Measured in CI (ubuntu-latest, 2 vCPUs, 2026-10-21). Every run of the PR tier and the first main run so far. Billed minutes are each job rounded up to a whole minute (runner pricing); a skipped job costs nothing.
| Run | Change | Cache | Wall time | Jobs (s) | Billed |
|---|---|---|---|---|---|
| 39657529808 | PR #104, everything | none | 542 s | select 14, code 519, docs 31, ci-required 4 | 12 min |
| 39663435122 | PR #104, everything | none | 566 s | select 21, code 536, ci-required 4 | 11 min |
| 39666333757 | PR #105, everything | none | 505 s | select 21, code 476, ci-required 3 | 10 min |
| 39669208703 | main after #105, all L1, saved the cache | none | 453 s | linux 453 | 8 min |
| 39670447026 | demo #106, docs only | — | 32 s | select 25, ci-required 4 (code skipped) | 2 min |
| 39670571690 | demo #107, one crate | warm | 113 s | select 12, code 92, ci-required 4 | 4 min |
| 39670483912 | demo #108, Cargo.lock (everything) | warm, via the fallback key | 181 s | select 22, code 150, ci-required 4 | 5 min |
Medians:
Successful PR runs: 343 s (6 runs).
Cold PR runs: 542 s, 11 billed minutes.
Warm code PR runs: 113–181 s, 4–5 billed minutes.
A cold
codejob: about 1 min of setup (Node 7 s, Tauri packages 35 s, toolchain 9 s,pnpm install4 s), then about 7.5 min of compiling (clippy about 280 s, tests about 140 s).With the cache restored: clippy takes 2–3 s and the tests 3–6 s. The cache is 319 MiB (key
rust-v1-Linux-<hash>, saved bymain).The rest is the 3 billed minutes of fixed cost per code run:
select, the setup insidecode, andci-required.Failure collection (#91) adds no billed minutes. A warm run with everything selected cost 4 billed minutes before and after it: 39671582041 (PR #109) against 39676944320 (PR #110).
ci-requiredgrew from 5 s to 17–19 s (checkout, Node, downloading the reports), still within its one billed minute.
Measured on the post-merge tier (#16, 2026-10-22; manual runs of main.yml on demo branches; Windows minutes count double):
| Run | Change | Jobs (s) | Billed |
|---|---|---|---|
| 39775990305 | One crate, with a deliberate Linux-only test failure | select 24, linux 96 (failed), windows 1084 (cold, no Windows cache yet), summary 23 | 42 min (Windows 38) |
| 39778706769 | Docs only | select 28, summary 25 (linux and windows skipped) | 2 min |
Still assumed (no runs yet):
| Job | Cache | Duration | Basis |
|---|---|---|---|
| Post-merge Linux, warm | warm | 4 min billed | The warm code job plus about 1 min of setup |
| Post-merge Windows | warm | about 10 min billed | The cold run measured above was 18 min; the warm figure waits for the first run after main saves the Windows cache |
| Release candidate (Linux + Windows, with installers) | warm | 40 min | Two check:full runs plus bundling |
| Weekly full (Linux + Windows) | warm | 30 min | Two check:full runs |
Monthly estimate at the current pace. One agent, about 4 merged PRs a working day, so about 80 a month. About 3 CI runs per PR after cancellation; about a quarter of PRs are docs-only.
| Item | Runs a month | Billed minutes each | Minutes a month |
|---|---|---|---|
| PR checks, code (measured 4–5, warm) | 60 PRs × 3 = 180 | 5 | 900 |
| PR checks, docs-only (measured) | 20 PRs × 3 = 60 | 2 | 120 |
| Post-merge Linux | 80 | 4 | 320 |
| Post-merge Windows (Windows-relevant merges, assumed half; #16) | 30 | 10 | 300 |
| Weekly full | 4 | 30 | 120 |
Cold-cache penalty (new Cargo.lock or toolchain, assumed 8; cold main and PR runs) | 8 | +6 | 48 |
Cold Windows post-merge (new Cargo.lock or toolchain, assumed 8; measured 38 billed, about 28 above warm) | 8 | +28 | 224 |
| Release candidates (assumed 2) | 2 | 40 | 80 |
| Total | ≈ 2,110 |
That is slightly over the 2,000-minute quota once the measured cold Windows runs are counted (#16). The PR tier still dominates. Ways to bring it under:
Push to the PR branch less often: batch fixes before pushing. Cancellation discards superseded runs (demonstrated on demo #107), but a started job still costs its minutes.
Run Windows post-merge on a daily batch of the merged commits, instead of per merge. This saves up to 300 minutes, at the cost of slower Windows feedback.
PO decisions: a payment method or plan change (then overage is billed instead of blocked), or making the repository public (standard runners are free; #90).
Windows minutes: the runner pricing page lists only dollar rates and states no multiplier against the included minutes. Until the first Windows run shows its effect on the usage report, budget Windows as if it counted double. Then the Windows line above becomes 600 minutes, and the total about 2,190, over the quota.
Artifact storage is 500 MB (shared with Packages). Retention is set per upload:
7 days for diagnostics and logs;
14 days for release-candidate installers;
releases go to GitHub Releases, which don't count as Actions artifacts.
The repository default is 90 days (retention) and is too long for 500 MB.
Watching the quota: check usage in the account's billing settings at least weekly while the CI tiers are new. #14 records the first measured month and updates the tables above.