From 99b3c5e909922844c311831af38a7b8963379a40 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Tue, 11 Aug 2026 17:06:13 +0200 Subject: [PATCH] docs(cache-maintenance): enhance documentation --- .github/workflows/cache-maintenance.md | 304 ++++++++++++++++--------- 1 file changed, 202 insertions(+), 102 deletions(-) diff --git a/.github/workflows/cache-maintenance.md b/.github/workflows/cache-maintenance.md index b7669f2..9538a14 100644 --- a/.github/workflows/cache-maintenance.md +++ b/.github/workflows/cache-maintenance.md @@ -1,130 +1,242 @@ # Bazel cache maintenance `cache-maintenance.yml` maintains the shared Bazel caches of a repository that -uses `MODULE.bazel.lock`. Follow the steps below to integrate it into an -existing repository. +uses `MODULE.bazel.lock`. Before integrating it, commit an up-to-date lockfile. -## Quick start +## How caching works -Before starting, commit an up-to-date `MODULE.bazel.lock`. - -### 1. Cache each Bazel job - -Replace direct use of `bazel-contrib/setup-bazel` (or another cache setup -action) in every Bazel job with the S-CORE cache action: +Every Bazel job must use the S-CORE cache action instead of using +`bazel-contrib/setup-bazel` directly: ```yaml +# Add this step after checkout in every Bazel job. - name: Setup Bazel with shared caching + # Replace `` with a reviewed cicd-actions commit SHA. uses: eclipse-score/cicd-actions/setup-bazel-cache@ with: - unique-cache-name: ${{ github.job }}[-matrix-job-name] + # This name must be stable and unique for the produced Bazel outputs. + unique-cache-name: ${{ github.job }} + # For a matrix job, append its stable identifying values, for example: + # unique-cache-name: ${{ github.job }}-${{ matrix.name }} + # Uncomment this when the cache-writing branch is not named `main`: + # main-branch: ``` -Add an optional stable suffix for matrix values or target configurations when a -job name alone is not unique. +Use a stable suffix for matrix values or target configurations when a job name +alone is not unique. The action restores caches in every job, but saves them +only when the current ref is `main` by default. Set `main-branch` if the +repository uses a different default branch. + +There are two ways to combine the action with cache maintenance: + +| Mode | Use when | Tradeoff | +| ---------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Automatic warmup | Existing Bazel workflows already run on every push to the default branch | Simplest integration, but every job may download the repository-cache delta after a lockfile change | +| Ordered warmup | Avoiding redundant dependency downloads is worth the additional orchestration | Maintenance downloads the delta once, but build workflows must be reusable and wait for maintenance | -### 2. Make cache-warming workflows callable +Both modes rebuild the caches after a lockfile change. Automatic warmup is the +simpler default. Ordered warmup remains useful when dependency downloads are +expensive or unreliable and should be performed only once. -For every workflow that should warm a Bazel cache after a push, remove its -`push` trigger and add `workflow_call`. Retain any pull-request or manual -triggers that are useful for the workflow. The cache-maintenance workflow then -invokes it after repository-cache maintenance has finished. +## Option 1: Automatic warmup -Workflow triggered only by the cache-writing push — before: +Keep the `push` triggers of existing Bazel workflows. On a push to `main`, +`setup-bazel-cache` automatically saves the repository cache and each job's +disk cache. Add a separate workflow that calls only the maintenance workflow: ```yaml +name: Cache maintenance + on: + # Validate every `variants` entry without writing caches before merge. + pull_request: + types: [opened, reopened, synchronize] + # Perform the same read-only validation for merge-queue commits. + merge_group: + types: [checks_requested] + # Allow a repository-cache rebuild after a transient failure or for testing. + workflow_dispatch: + # Refresh and prune shared caches after every push to the writing branch. push: + # Replace `main` when the repository uses a different writing branch. branches: [main] + +jobs: + repository_cache_maintenance: + permissions: + # Required to delete invalidated and superseded cache generations. + actions: write + # Required to check out the repository and inspect its lockfile. + contents: read + secrets: + # Remove these optional secrets when no configured variant uses QNX. + qnx-license: ${{ secrets.SCORE_QNX_LICENSE }} + qnx-user: ${{ secrets.SCORE_QNX_USER }} + qnx-password: ${{ secrets.SCORE_QNX_PASSWORD }} + # Replace `` with a reviewed cicd-workflows commit SHA. + uses: eclipse-score/cicd-workflows/.github/workflows/cache-maintenance.yml@ + with: + # Each non-empty line is appended to one `bazel fetch` invocation. + # Replace the example configuration with those used by this repository. + variants: | + //... + --config=target_config_1 //... ``` -Workflow triggered only by the cache-writing push — after: +The reusable workflow already prunes obsolete cache generations, so this mode +does not need a separate prune job. + +Automatic warmup is not yet optimized for the smallest possible cache size. +Optimizations will follow. + +When the lockfile changes, its new hash selects a new repository-cache +generation. Each independently starting Bazel job restores the previous +generation, downloads the delta it needs, and automatically saves caches at the +end of the job. The caches are therefore rebuilt without explicit warmup +orchestration. However, jobs running in parallel cannot yet restore the new +generation, so they may download the same repository-cache delta redundantly. + +Use this mode when the simpler workflow structure is more valuable than +avoiding those redundant downloads. Every cacheable Bazel workflow must run on +every push to `main`; otherwise, its job-specific disk cache is never updated. +Pull-request-only workflows can restore caches, but cannot update them. + +## Option 2: Ordered warmup + +Use a single orchestration workflow to run repository-cache maintenance first, +selected builds second, and final pruning last. Maintenance downloads the +repository-cache delta once and publishes the new generation. The subsequent +build jobs restore that generation instead of downloading the delta in every +job, while also warming their individual disk caches. + +### 1. Make cache-warming workflows callable + +For every workflow that should warm a disk cache, replace its `push` trigger +with `workflow_call`. Retain pull-request or manual triggers that are useful. +The orchestration workflow becomes the only entry point for its default-branch +pushes. + +Before: ```yaml +# The cacheable build workflow currently runs directly after each main push. on: - workflow_call: + push: + branches: [main] ``` -Workflow with pull-request checks — before: +After: ```yaml +# The orchestration workflow now calls this build workflow after maintenance. on: - pull_request: - types: [opened, synchronize, reopened] + workflow_call: ``` -Workflow with pull-request checks — after: +A workflow that also runs for pull requests can retain that trigger: ```yaml on: + # Keep the workflow's existing pull-request checks. pull_request: types: [opened, synchronize, reopened] + # Let the cache-maintenance orchestrator call the same workflow on `main`. workflow_call: ``` -### 3. Add the orchestration workflow +### 2. Add the orchestration workflow Create `.github/workflows/cache-maintenance.yml` in the consuming repository. -It validates repository dependencies first, warms the selected build caches -afterward, and prunes obsolete caches last. Pin reusable workflows and actions -to reviewed commit SHAs. +Pin reusable workflows and actions to reviewed commit SHAs. ```yaml name: Cache maintenance on: - # Read-only self-test of every `variants` entry before merge. + # Validate every `variants` entry without writing caches before merge. pull_request: - types: [opened, reopened, synchronize, labeled, unlabeled] - # The same self-test for the merge-queue commit. + types: [opened, reopened, synchronize] + # Perform the same read-only validation for merge-queue commits. merge_group: types: [checks_requested] - # Rebuild caches manually, for example after a transient download failure or for workflow testing. + # Dispatch from `main` to rebuild both repository and disk caches manually. workflow_dispatch: - # Select the branch whose pushes may warm and prune shared caches. + # Run the ordered refresh and warmup after every cache-writing push. push: + # Replace `main` when the repository uses a different writing branch. branches: [main] jobs: repository_cache_maintenance: permissions: - # Required when a lockfile change replaces and prunes caches. + # Required to delete invalidated and superseded cache generations. actions: write + # Required to check out the repository and inspect its lockfile. contents: read secrets: - # Optional unless a configured variant uses QNX. + # Remove these optional secrets when no configured variant uses QNX. qnx-license: ${{ secrets.SCORE_QNX_LICENSE }} qnx-user: ${{ secrets.SCORE_QNX_USER }} qnx-password: ${{ secrets.SCORE_QNX_PASSWORD }} + # Replace `` with a reviewed cicd-workflows commit SHA. uses: eclipse-score/cicd-workflows/.github/workflows/cache-maintenance.yml@ with: - # Each line becomes one `bazel fetch` invocation. + # Fetch all configurations required by the subsequent warmup jobs. + # Each non-empty line is appended to one `bazel fetch` invocation. variants: | //... --config=target_config_1 //... warmup-qnx-x86_64: + # Do not start until the new repository-cache generation is available. needs: repository_cache_maintenance - # PR and merge-queue runs validate variants above but never warm or write - # shared caches. + # PR and merge-queue runs above are read-only validation runs. if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + # Forward repository secrets needed by this local reusable build workflow. secrets: inherit + # Replace this path with a local reusable workflow that uses + # eclipse-score/cicd-actions/setup-bazel-cache in its Bazel jobs. uses: ./.github/workflows/build_qnx_x86_64.yml delete_old_caches: - needs: warmup-qnx-x86_64 + # List every warmup job here when more than one is configured. + needs: + - warmup-qnx-x86_64 + # - warmup-other-configuration + # Run after successful or failed warmups, but never after cancellation and + # never for the read-only pull-request or merge-queue events. if: ${{ !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} runs-on: ubuntu-24.04 permissions: - # Prune after every warmup job has completed its cache-save post-step. + # Required to delete superseded cache generations. actions: write + # Keep all permissions not needed by this job read-only. contents: read steps: + # Run only after every warmup job has completed its cache-save post-step. - name: Prune obsolete Bazel caches + # Replace `` with a reviewed cicd-actions commit SHA. uses: eclipse-score/cicd-actions/prune-cache@ ``` -## Corner cases and operations +Add one `warmup-*` job for every build configuration that should populate a +disk cache. Each must depend on `repository_cache_maintenance`. The final prune +job must list all warmup jobs in `needs`; it removes older disk-cache +generations created after maintenance performed its own prune. + +## Configuration and operations + +### Variants + +`variants` is a newline-separated list of argument groups passed to `bazel +fetch`. Include every platform or configuration whose external repositories +should be available from the maintained repository cache. A line containing +only `//...` is valid. Do not place a shell command in this input. + +Omitting rarely used configurations keeps the repository cache smaller and +faster to restore. In automatic mode, independently running Bazel jobs may each +download the delta needed for their targets. In ordered mode, maintenance +fetches all variants once before the build warmups start. ### Credentials and private dependencies @@ -134,82 +246,70 @@ when a variant fetches dependencies from private GitHub repositories. To use a token instead, pass it as the `token` secret and omit `github-app-client-id` and `github-app-private-key`. -For a private S-CORE derivative that needs private GitHub dependencies, add the -GitHub App credentials to its calling job: +For a private S-CORE derivative that needs private GitHub dependencies, use the +following maintenance job instead of the one in either complete example: ```yaml +jobs: + repository_cache_maintenance: + permissions: + # Required to delete invalidated and superseded cache generations. + actions: write + # Required to check out the repository and inspect its lockfile. + contents: read secrets: + # Store the GitHub App private key as a repository or organization secret. github-app-private-key: ${{ secrets.PRIVATE_DEPENDENCY_APP_PRIVATE_KEY }} + # Replace `` with a reviewed cicd-workflows commit SHA. + uses: eclipse-score/cicd-workflows/.github/workflows/cache-maintenance.yml@ with: + # A GitHub App client ID is not secret and can be stored as a variable. github-app-client-id: ${{ vars.PRIVATE_DEPENDENCY_APP_ID }} + # Include every target configuration whose dependencies must be cached. + variants: | + //... ``` +The reusable workflow configures private-dependency credentials whenever the +caller supplies them. Do not pass credentials to workflows that execute +untrusted pull-request code. Public repositories do not receive repository +secrets for pull requests from forks; private repositories and same-repository +branches require an explicit caller-side credential policy. + The older `score-qnx-license`, `score-qnx-user`, and `score-qnx-password` secret names remain supported for compatibility. New callers should use the `qnx-*` names. -### Variants and multiple warmup jobs - -`variants` is a newline-separated list of argument groups passed to `bazel -fetch`. Include every platform or configuration whose external repositories must -be available. A line containing only `//...` is valid. Do not place a shell -command in this input. - -Add one `warmup-*` job for every build configuration that should populate a disk -cache. Each warmup job must depend on `repository_cache_maintenance`, and the -final prune job must list all warmup jobs in `needs`. - -### Pull requests and merge queues +### Pull requests, merge queues, and manual runs The reusable workflow treats pull requests and merge queues as dry runs: it -validates and fetches the configured variants, but does not write shared caches. -Pushes and manual dispatches rebuild and prune caches when `MODULE.bazel.lock` -has changed. The caller's push trigger and the ref selected for a manual -dispatch determine the cache-writing branch. - -GitHub's secret handling depends on the PR model: public repositories do not -receive repository secrets for PRs from forks, whereas private repositories and -same-repository branch PRs can make secrets available. This workflow -nevertheless deliberately does not configure GitHub App credentials or tokens -for `pull_request` or `pull_request_target` runs, because they execute code -from the proposed change. - -Repositories that use a trusted, branch-based PR model may choose a different -credential policy. With the default policy, validate private dependencies in a -trusted event such as a merge-queue run or a manually dispatched run. - -### Operational checklist - -- Run `bazel mod tidy` when changing module dependencies, and commit the - resulting `MODULE.bazel.lock`. -- Include the Bazel configurations needed for the desired cache coverage in - `variants`; omitting rarely used configurations keeps the repository cache - smaller and faster to restore. -- Use stable, distinct `unique-cache-name` values for each cache-producing job. -- Do not run an independent cache-pruning job in parallel with maintenance. -- Keep the final prune job after all warmup jobs; it needs `actions: write`. -- Update the pinned action and workflow SHAs together when adopting a newer - caching implementation. - -## Background: cache lifecycle +validates and fetches the configured variants, but does not write or delete +shared caches. Pushes and manual dispatches rebuild the repository cache when +`MODULE.bazel.lock` has changed and always prune obsolete cache generations. -The workflow manages two cache types: - -| Cache | Purpose | Refresh rule | -| ---------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -| Repository cache | Downloaded external repositories and archives | Rebuild when `MODULE.bazel.lock` changes | -| Disk cache | Local Bazel action outputs | Add outputs on cache-writing pushes or manual runs; delete it after a lockfile-driven repository-cache refresh | +The caller's push trigger selects the cache-writing branch. A manually +dispatched maintenance run writes for the selected ref; ordinary Bazel jobs +using the default `setup-bazel-cache` configuration save only on `main`. -All Bazel jobs restore these caches through -`eclipse-score/cicd-actions/setup-bazel-cache`. Each job or target configuration -needs a stable, unique cache name so disk caches do not collide. +## Cache lifecycle -On a cache-writing push or manual dispatch, the reusable workflow compares -`MODULE.bazel.lock` with the previous commit. If it changed, it constructs and -uploads a fresh repository cache, then deletes stale disk caches only after that -upload is complete. The subsequent warmup jobs repopulate their disk caches, -and the final prune job removes obsolete cache entries. +The workflow manages two cache types: -If the lockfile did not change, the repository cache is left intact and warmup -jobs can add new action outputs to their own disk caches. This avoids a cache -gap and unnecessary dependency downloads. +| Cache | Purpose | Refresh rule | +| ---------------- | --------------------------------------------- | ------------------------------------------------------------------------------------- | +| Repository cache | Downloaded external repositories and archives | Rebuild when `MODULE.bazel.lock` changes | +| Disk cache | Local Bazel action outputs | Add outputs on cache-writing pushes; delete all disk caches when the lockfile changes | + +All Bazel jobs restore both through +`eclipse-score/cicd-actions/setup-bazel-cache`. Repository caches share a key +derived from the lockfile. Disk caches use the configured +`unique-cache-name`, and cache-writing builds save a new generation after every +run. + +When the lockfile changes, both modes create a repository-cache generation for +the new lockfile and repopulate disk caches. Automatic warmup lets each +independently triggered Bazel job download the required repository delta and +save its caches. Ordered warmup has maintenance download the delta once before +the selected build jobs restore the new repository cache and warm their disk +caches. When the lockfile does not change, the repository cache remains intact +and maintenance only prunes obsolete generations.