Skip to content

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 & pathPurposeScope
POST /ci/runsReport a run (Mode A)ci:write
PATCH /ci/runs/:idUpdate a run’s status/durationci:write
POST /ci/runs/:id/jobsUpsert jobs for a runci:write
POST /ci/runs/:id/completeMark a run terminalci:write
GET /ci/connections/:idFetch a connectionci:read
PATCH /ci/connections/:idUpdate branch/ingest-mode/statusci:manage
DELETE /ci/connections/:idDelete a connectionci:manage
GET /ci/connections/:id/pipelinesList pipelinesci:read
POST /ci/connections/:id/discoverTrigger discovery (202)ci:manage
GET /ci/connections/:id/discoveryLatest discovery snapshotci:read
GET /ci/pipelines/:id/runsList runs for a pipelineci:read
GET /ci/runs/:idFetch a runci:read
GET /ci/runs/:id/jobsList a run’s jobsci:read
GET /teams/:id/ci/connectionsList a team’s connectionsci:read
POST /teams/:id/ci/connectionsCreate a connectionci:manage
GET /environments/:id/timelineUnified build+CI timelineauthenticated

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.