CI Cached Indexes (Team Tier)
On large repos, a fresh canopy index in CI can take 60–120 seconds. Team tier includes a remote cache that stores the index after the first build and restores it on subsequent runs. When only a small number of files change between commits, the restore + incremental update is typically under 5 seconds.
How the cache works
Section titled “How the cache works”The cache key is a hash of three inputs:
- The git commit SHA of the repo being indexed
- The Canopy binary version
- The hash of
.canopy/config.tomland.canopy/team.yml
When these three inputs match a stored cache, Canopy skips re-indexing entirely and restores the stored index directly. When files change between commits, Canopy restores the closest ancestor cache and runs an incremental update — only changed files are re-parsed.
Cache integrity is verified automatically. A corrupted or tampered cache is rejected and Canopy falls back to a full index.
Prerequisites
Section titled “Prerequisites”- Team tier license (run
canopy licenseand confirm theTier:line readsTeam (team)) - Team ID (found in the Canopy admin portal)
CANOPY_LICENSE_KEYstored as a CI secret (GitHub Actions) or CI/CD variable (GitLab)
Simple approach: canopy ci --team
Section titled “Simple approach: canopy ci --team”The simplest way to use remote cache is to pass --team to canopy ci. Canopy handles upload and restoration automatically:
canopy ci --team <team_id> --format github --fail-on-p0No separate canopy index step needed. Canopy checks the remote cache, restores if available, runs an incremental update if needed, runs health checks, and uploads the updated index back to the cache.
name: Canopy Health Check
on: pull_request: branches: [main] push: branches: [main]
jobs: canopy-health: runs-on: ubuntu-latest permissions: pull-requests: write checks: write
steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: Install Canopy run: | curl -fsSL $CANOPY_DOWNLOAD_URL/releases/latest/canopy-linux-x86_64 \ -o canopy && chmod +x canopy && sudo mv canopy /usr/local/bin/canopy
- name: Run Canopy CI with remote cache run: canopy ci --team ${{ vars.CANOPY_TEAM_ID }} --format github --fail-on-p0 env: CANOPY_LICENSE_KEY: ${{ secrets.CANOPY_LICENSE_KEY }}Store CANOPY_TEAM_ID as a repository variable (not a secret — it’s not sensitive) and CANOPY_LICENSE_KEY as a secret.
canopy-health: stage: quality image: ubuntu:22.04
before_script: - apt-get update -qq && apt-get install -y -qq curl - curl -fsSL $CANOPY_DOWNLOAD_URL/releases/latest/canopy-linux-x86_64 -o /usr/local/bin/canopy - chmod +x /usr/local/bin/canopy
script: - canopy ci --team $CANOPY_TEAM_ID --format gitlab --fail-on-p0 | tee canopy-report.json - exit ${PIPESTATUS[0]}
artifacts: reports: codequality: canopy-report.json when: alwaysSet CANOPY_TEAM_ID as a CI/CD variable (unmasked — not sensitive) and CANOPY_LICENSE_KEY as a masked variable.
Manual cache control
Section titled “Manual cache control”For more control, use --cache-to and --use-cache explicitly:
Upload after indexing:
canopy index . --with-search --with-git \ --cache-to r2://<team_id>/<repo-hash>Restore before indexing:
canopy index . --with-search --with-git \ --use-cache r2://<team_id>/<repo-hash><repo-hash> is a stable identifier for the repository — typically $(git remote get-url origin | sha256sum | cut -c1-16) to make it consistent across runners.
Full workflow using explicit cache control:
- name: Restore Canopy index from remote cache run: | REPO_HASH=$(git remote get-url origin | sha256sum | cut -c1-16) canopy index . --with-search --with-git \ --use-cache r2://${{ vars.CANOPY_TEAM_ID }}/${REPO_HASH} || true # || true: if no cache exists (first run), fall through to full index
- name: Full index if cache miss run: canopy index . --with-search --with-git
- name: Upload index to remote cache run: | REPO_HASH=$(git remote get-url origin | sha256sum | cut -c1-16) canopy index . --cache-to r2://${{ vars.CANOPY_TEAM_ID }}/${REPO_HASH}
- name: Run health check run: canopy ci --repo . --format github --fail-on-p0Cache hit statistics
Section titled “Cache hit statistics”When --team is used, Canopy logs cache performance to stdout. The progression on a cache hit:
canopy: CI mode with team cache for team <team_id>canopy: checking R2 cache at r2://<team_id>/<repo-hash>...canopy: cache hit — skipping index rebuild (integrity verified)(The two done — ... (N ms) lines from canopy index follow when an incremental update is needed against the restored index.)
A cold miss (no cache available, or cache fetch failed) falls through to a full index:
canopy: CI mode with team cache for team <team_id>canopy: checking R2 cache at r2://<team_id>/<repo-hash>...canopy: cache miss — performing full indexcanopy: indexing /path/to/repo (incremental)canopy: AST index done — 8432 files indexed, 12 skipped, 0 errors (47180 ms)If R2 is unavailable, the rebuild still runs — caching is best-effort, never a hard prerequisite:
canopy: warning: cache unavailable (<error>) — performing full indexAfter a successful run, the rebuilt index is uploaded back to the same r2:// slot so the next CI run can hit it.
Cache retention
Section titled “Cache retention”Remote cache entries expire after 30 days of no access. Active repos (daily CI runs) never expire in practice. Manually purge a cache entry from the admin portal if you need to force a full re-index (e.g., after significant refactoring that breaks incremental updates).
Common pitfalls
Section titled “Common pitfalls”Cache never hits
The cache key includes the Canopy binary version. If your CI downloads latest each run and Canopy releases a patch, the version changes and every run is a cache miss. Pin to a specific version:
curl -fsSL $CANOPY_DOWNLOAD_URL/releases/v1.4.0/canopy-linux-x86_64 \ -o canopyCache hit but index seems stale
Verify that fetch-depth: 0 is set in the checkout step. With shallow clones (fetch-depth: 1), canopy index --with-git can’t read full git history, which affects the git activity data in canopy_prepare results. It doesn’t affect the cache key — that’s based on commit SHA, not history depth.
“Team not found” error
The team ID in CANOPY_TEAM_ID doesn’t match your account. Log in to the admin portal and copy the team ID exactly as shown.