Architecture

Design principles

  • Opt-in reporting - only workloads with github-deployment-bridge.io/auto-report=true are reported; others are skipped quietly.
  • OCI labels are canonical for build metadata - repository and commit come from standard image labels.
  • Kubernetes annotations are optional overrides - deployment-specific environment, URLs, and opt-outs.
  • Zero per-app mapping database - no repository-specific configuration beyond annotations on the workload.
  • Observe only - the bridge never triggers deployments or mutates cluster workloads.
  • Full GitHub Deployments lifecycle - one Deployment per (owner, repo, environment, commit, deploymentName) with status updates as Flux progresses.
  • Safe reconcile loop - missing/invalid metadata skips a workload with a warning; transient GitHub/OCI errors retry with backoff (GitHub Retry-After / rate-reset honored).
  • Single-writer cache - SQLite on a PVC is the intentional HA model: one replica, Recreate upgrades, node loss means reschedule downtime (not active-active). Multi-writer would require a different store.

Flux sources

The bridge watches:

KindAPIInventory
Kustomizationkustomize.toolkit.fluxcd.io/v1.status.inventory
HelmReleasehelm.toolkit.fluxcd.io/v2.status.inventory (Flux ≥ 2.8 / helm-controller ≥ 1.5)

Events fire when conditions, observedGeneration, or revision fields change. Reporting runs only when inventory yields at least one resolvable workload image.

Phase derivation

Desired phaseFlux signal
successReady=True and generation == observedGeneration
failureReady=False (observed) or Stalled=True, or known failure reasons (HealthCheckFailed, InstallFailed, …)
in_progressReconciling=True, or Ready not True while not yet a failure

The reporter maps desired phases onto GitHub statuses with an idempotent state machine:

  • Catch-up: if the cache is empty and Flux is already terminal, emit only that terminal status (no synthetic history).
  • Early states: queued then in_progress when first observing an in-progress reconcile for a new commit.
  • Never transition successin_progress. Never send duplicate identical statuses.

Workload discovery

  1. Parse .status.inventory for Deployment, StatefulSet, and DaemonSet.
  2. Resolve ReplicaSet entries via owner references to their controlling Deployment.
  3. Ignore Job / CronJob.
  4. Collect github-deployment-bridge.io/* annotations from the workload (and pod template as fallback).

Empty inventory (including HelmRelease on Flux before 2.8) → skip.

Metadata resolution

Priority for every field:

  1. Kubernetes annotation
  2. OCI label
  3. Controller default (if applicable)

OCI labels

LabelRequiredPurpose
org.opencontainers.image.sourceyes*GitHub owner/repository
org.opencontainers.image.revisionyes*Git commit SHA (Deployment ref)
org.opencontainers.image.versionnoLogging / payload
org.opencontainers.image.titlenoLogging
org.opencontainers.image.creatednoDiagnostics

*Required unless overridden by the matching Kubernetes annotation.

Kubernetes annotations

Prefix: github-deployment-bridge.io/

AnnotationOverridesPurpose
repositoryOCI sourceowner/repo when multiple apps share an image
commitOCI revisionExceptional commit override
environmentENVIRONMENTGitHub Deployment environment
environment-urlENVIRONMENT_URLDeployment Status environment_url
log-urlLOG_URL_TEMPLATEDeployment Status log_url (both support {sha} / {namespace} / {name} / {service} / {environment} / {cluster})
descriptionDESCRIPTION (default Deployed by FluxCD)Deployment description
productionderived from env nameproduction_environment (true/false)
auto-report(none)Opt-in. Must be true to report; absent/false ignores the workload (no OCI fetch, no warning spam)
deployment-namerepository nameIndependent reports for monorepo workloads (also GitHub task; see Monorepos)
clusterCLUSTER_NAMEDeployment payload cluster (API only)
team(none)Deployment payload team (API only)
service(none)Deployment payload service (API only)
component(none)Deployment payload component (API only)
slack-channel(none)Deployment payload slackChannel (API only)
owner(none)Deployment payload owner (service owner, not GitHub repo owner; API only)
release(none)Deployment payload release (API only)
tag(none)Deployment payload tag (API only)

Validation

FieldRule
RepositoryMust resolve to owner/repository
CommitValid Git SHA (7-40 hex)
EnvironmentNon-empty
Environment / log URLAbsolute https:// URL when set

Missing or invalid required metadata → skip reporting and emit a warning. Never fail reconciliation for that reason.

GitHub Deployment mapping

Resolved fieldGitHub fieldVisible in GitHub UI
RepositoryDeployment repositoryyes
CommitDeployment refyes
EnvironmentDeployment environmentyes
Productionproduction_environmentyes (Environments)
DescriptionDeployment descriptionyes
Deployment name (when annotated)Deployment taskno (API / webhooks only)
Environment URLStatus environment_urlyes
Log URLStatus log_urlyes

Deployment payload includes cluster, namespace (workload), sourceNamespace (Flux Kustomization / HelmRelease namespace), source name (kustomization / helmRelease), deploymentName, image, optional digest / version, controllerVersion, and any optional annotation fields (team, service, component, slackChannel, owner, release, tag). The cluster annotation overrides the controller CLUSTER_NAME env.

GitHub UI does not render custom payload fields. Values such as team, service, component, slackChannel, owner, release, tag, and cluster are stored on the Deployment and returned by the API / webhooks for integrations — they do not appear as labels in the repository Deployments page. Prefer environment, description, or environment-url when you need something visible in the UI.

Crash recovery also matches older payloads that used the Flux source namespace as namespace and omitted sourceNamespace.

Status updates set auto_inactive=false. GitHub's environment-scoped auto-inactive would otherwise deactivate sibling monorepo deployments that share an environment. When a newer commit reaches success for the same identity (deploymentName included), the bridge explicitly marks prior cached success deployments inactive.

Deduplication cache key: (owner, repo, environment, commitSHA, deploymentName). Before creating a Deployment, the bridge writes a provisional cache row (deployment_id=0). It then searches GitHub for an existing Deployment with the same ref, environment, and payload (crash recovery) and only creates when none is found. The resolved deployment_id is persisted before status updates.

Monorepos

When multiple workloads in one GitHub repository should report independently (for example frontend and backend), you have two options:

ApproachAnnotationMultiple active deploymentsGitHub UI
Distinct deployment namesdeployment-name (frontend / backend)yes — cache identity includes deploymentName; bridge supersedes only within that nameWeak — task / name is not shown as a first-class label; both appear under the same environment
Distinct environmentsenvironment (production-frontend / production-backend)yes — environments are independentBest — each environment is listed separately with its own active deployment

Recommendation: use different environment values for the best Deployments UI experience. Use deployment-name when you want independent tracking (and correct active supersession) while keeping a shared environment name; combine both when useful (environment for UI grouping, deployment-name for API identity).

Without deployment-name, both workloads default to the repository name and share one cache identity — only one active deployment is tracked for that key.

# Best UI: separate environments
metadata:
  annotations:
    github-deployment-bridge.io/auto-report: "true"
    github-deployment-bridge.io/environment: "production-frontend"
    github-deployment-bridge.io/deployment-name: "frontend"
---
metadata:
  annotations:
    github-deployment-bridge.io/auto-report: "true"
    github-deployment-bridge.io/environment: "production-backend"
    github-deployment-bridge.io/deployment-name: "backend"