Canopy Connect
Canopy Connect (“CI” in the code/CLI) links your existing CI system (GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, CircleCI, or anything else via a generic adapter) to a Canopy environment, so runs and jobs from outside Canopy’s own build pipeline show up alongside Canopy’s own deploys in one timeline.
This is distinct from Canopy’s own git-push-triggers-a-deploy pipeline (see Deploys & the Staged Pipeline) - that keeps working independently of whether Connect is set up.
Status: gated behind the host.ci platform feature, which is ga
and enabled. As of 2026-08-01, Connect’s background jobs (discovery,
health reconciliation, stale-run sweeping, status write-back to your git
provider, retention pruning) went live in production for the first time
- previously the feature existed in the API but its async machinery was dormant. It’s genuinely operational now, not “coming soon.”
Connect a repository
canopy ci connect --team <team_id> --provider <id> --repo <host/owner/name>This creates a connection - one per (team, provider, repository).
Reconnecting an already-connected repo returns the existing connection
rather than erroring.
canopy ci connections list --team <team_id>Pipeline discovery
canopy ci discover <connection_id>Kicks off an async job that does one bounded repository-tree listing
(max 2000 entries, no cloning) against the connection’s default branch,
matches known CI-provider file patterns (e.g.
.github/workflows/*.yml, .gitlab-ci.yml,
bitbucket-pipelines.yml), and parses each match into a pipeline
record - name, jobs, triggers, whether a deploy step was detected.
Framework/manifest detection (package.json, Dockerfile) isn’t built
yet; discovery only looks at CI pipeline definitions.
Reporting runs
Two ways runs get into Canopy:
Mode A - direct report (what canopy ci report and the SDK’s
sdk.ci.report() do): your CI job calls Canopy’s API directly with a
ci:write-scoped credential.
canopy ci report --connection <ci_connection_id> [--status <status>] [--env <environment_id>] [--commit <sha>]Auto-detects the CI environment it’s running in (checks
GITHUB_ACTIONS, GITLAB_CI, BITBUCKET_BUILD_NUMBER, JENKINS_URL,
CIRCLECI, BUILDKITE, DRONE, TF_BUILD env vars, in that order).
Reports are idempotent on (connection, external_run_id, external_attempt)
- safe to call more than once for the same run.
Mode B - webhook-driven: for connections with ingestMode: 'webhook'
or 'both', Canopy backfills a provider webhook subscription and
ingests workflow_run/pipeline-hook/commit-status events as they
arrive, normalizing them the same way Mode A does.
Any CI system that doesn’t have a first-class adapter still works via
the generic adapter - post the canonical run/job shape directly with
canopy ci report, no provider-specific integration needed.
canopy ci runs list --pipeline <pipeline_id>canopy ci runs show <run_id>A run’s status only moves forward (running → success/failed/
cancelled) - it never regresses on an out-of-order or duplicate
delivery. A run stuck running past a health threshold eventually flips
to unknown, never silently to failed.
Validating a pipeline file locally
canopy ci validate [--file <path>]Fully offline - no network call. Mirrors the server-side adapters’
parsing heuristics locally so you can check a .github/workflows/*.yml
(or GitLab/Bitbucket equivalent) will actually be picked up by
discovery, before pushing it.
The unified timeline
canopy timeline --env <environment_id> [--cursor <ISO timestamp>]GET /environments/:id/timeline merges Canopy’s own builds and Connect
pipeline runs for one environment into a single newest-first feed,
grouped by commit SHA. There’s no separate timeline table - it’s
computed at read time.
REST API
| Method & path | Purpose | Scope |
|---|---|---|
POST /ci/runs | Report a run (Mode A) | ci:write |
PATCH /ci/runs/:id | Update a run’s status/duration | ci:write |
POST /ci/runs/:id/jobs | Upsert jobs for a run | ci:write |
POST /ci/runs/:id/complete | Mark a run terminal | ci:write |
GET /ci/connections/:id | Fetch a connection | ci:read |
PATCH /ci/connections/:id | Update branch/ingest-mode/status | ci:manage |
DELETE /ci/connections/:id | Delete a connection | ci:manage |
GET /ci/connections/:id/pipelines | List pipelines | ci:read |
POST /ci/connections/:id/discover | Trigger discovery (202) | ci:manage |
GET /ci/connections/:id/discovery | Latest discovery snapshot | ci:read |
GET /ci/pipelines/:id/runs | List runs for a pipeline | ci:read |
GET /ci/runs/:id | Fetch a run | ci:read |
GET /ci/runs/:id/jobs | List a run’s jobs | ci:read |
GET /teams/:id/ci/connections | List a team’s connections | ci:read |
POST /teams/:id/ci/connections | Create a connection | ci:manage |
GET /environments/:id/timeline | Unified build+CI timeline | authenticated |
ci:write is meant for CI credentials (a scoped API key), not human
session tokens - see the SDK ci resource or canopy ci report rather than hand-rolling these calls from a human-facing
context.
OIDC federation
Instead of storing a long-lived Canopy API key in your CI system, GitHub
Actions and GitLab CI jobs can authenticate to /ci/runs* using their
own provider-minted short-lived OIDC id_token. Issuer/JWKS pairs are
fixed and hardcoded (token.actions.githubusercontent.com,
gitlab.com) - no dynamic issuer support, so self-hosted GitLab isn’t
supported this way yet.
Status write-back
Once a run reaches a terminal status, Canopy posts a commit status back to your git provider (e.g. “canopy/connect: CI run failed”) - best effort, doesn’t block anything if it fails.