Skip to content
Canopy is in pre-release. These docs describe the product at its public launch — commands, tool names, and integration examples reflect what you'll see once binaries ship. Join the waitlist →

GitHub Actions Integration

Running canopy ci in GitHub Actions surfaces broken imports, dead exports, circular dependencies, and custom plugin violations as inline PR annotations. Reviewers see findings directly on the diff — no external dashboards required.

  • Canopy license key (Solo or Pro — canopy ci is not available in Community tier)
  • License key stored as a GitHub Actions secret named CANOPY_LICENSE_KEY
  • A repo with a Canopy index committed or built in CI (see CI Cached Indexes for caching)

In your GitHub repo: Settings → Secrets and variables → Actions → New repository secret.

Name: CANOPY_LICENSE_KEY
Value: your Canopy license key

Create .github/workflows/canopy.yml:

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:
- name: Checkout
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
canopy --version
- name: Activate license
run: canopy activate ${{ secrets.CANOPY_LICENSE_KEY }}
env:
CI: true
- name: Index repo
run: canopy index . --with-search --with-git
- name: Run health check
run: canopy ci --repo . --format github --fail-on-p0

With --format github, Canopy writes a one-line summary header followed by GitHub Actions workflow commands to stdout. The workflow commands render as inline annotations on the PR; the summary line stays in the raw log so you can read the verdict without expanding the annotations panel:

canopy ci: FAIL — 1 critical, 0 errors, 2 warnings (4187ms) [/home/runner/work/myapp/myapp]
::error,file=src/payments/checkout.ts,line=45,title=canopy/broken_imports::cannot resolve '../lib/stripe-client'
::warning,file=src/utils/format.ts,line=12,title=canopy/dead_exports::'formatCurrency' has 0 consumers
::warning,file=src/auth/tokens.ts,line=88,title=canopy/dead_exports::'legacyTokenFormat' has 0 consumers

Severity mapping: Critical and Error findings emit ::error (red on the PR), Warning emits ::warning (yellow), Info emits ::notice (blue). The step exits non-zero on critical/error findings when --fail-on-p0 is set, blocking the merge.

The annotation title= field is canopy/<check_id> so you can group findings in the GitHub UI by which check produced them. Commas inside file paths are URL-escaped (%2C) and %/newlines inside messages are escaped (%25/%0A) per the GitHub Actions annotation spec.

For larger repos, separate the indexing and health check steps so you can cache the index:

jobs:
canopy-index:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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: Cache Canopy index
uses: actions/cache@v4
with:
path: .canopy/index
key: canopy-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.js', '**/*.py', '**/*.rs') }}
restore-keys: canopy-${{ runner.os }}-
- name: Activate and index
run: |
canopy activate ${{ secrets.CANOPY_LICENSE_KEY }}
canopy index . --with-search --with-git
canopy-health:
needs: canopy-index
runs-on: ubuntu-latest
permissions:
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v4
- name: Restore Canopy index
uses: actions/cache@v4
with:
path: .canopy/index
key: canopy-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.js', '**/*.py', '**/*.rs') }}
- name: Install Canopy and run CI check
run: |
curl -fsSL $CANOPY_DOWNLOAD_URL/releases/latest/canopy-linux-x86_64 \
-o canopy && chmod +x canopy && sudo mv canopy /usr/local/bin/canopy
canopy activate ${{ secrets.CANOPY_LICENSE_KEY }}
canopy ci --repo . --format github --fail-on-p0
FlagBehavior
--fail-on-p0Exit 1 if any P0 (critical) findings
--fail-on-p1Exit 1 if any P0 or P1 (error) findings
(none)Always exits 0, findings are informational only

Start with --fail-on-p0 to establish a baseline without blocking every PR. Add --fail-on-p1 once the codebase is clean.

License activation fails in CI The canopy activate command registers the machine fingerprint. CI runners are ephemeral — they get a new fingerprint each run. Use a license tier that allows multiple machine registrations (Pro or Team), or use canopy ci --team <team_id> which authenticates against the team token instead.

PR annotations don’t appear The workflow needs permissions.pull-requests: write and permissions.checks: write. If your org uses a default permissions policy that restricts this, update the workflow permissions block or ask your GitHub org admin.

Index takes too long Large repos can take 30–90s to index on a fresh CI runner. Use the cache approach above, or upgrade to Team tier for remote cache support.