Deployment Capability¶
DeploymentCapability is the contract base for a capability that acts on
a deployable repository: enumerate refs and commits, compare them, cut
tags and releases, and trigger CI workflow runs. Bind it with a
Capability(kind='deployment', handler=...) in the plugin's manifest.
Deployment is typically paired with an identity capability
(set requires_identity=True) so deploy actions run as the human user
rather than a shared service principal.
Surfaces: ui, api.
See Authoring Plugins for the manifest, capabilities, context, credential decryption, and error conventions shared by every plugin.
from imbi.common.plugins import (
CompareResult,
Commit,
DeploymentCapability,
DeploymentRun,
PluginContext,
Ref,
)
class GitHubDeployment(DeploymentCapability):
async def list_refs(
self,
ctx: PluginContext,
credentials: dict[str, str],
kind: str = 'all',
query: str | None = None,
) -> list[Ref]:
...
async def list_commits(
self,
ctx: PluginContext,
credentials: dict[str, str],
ref: str,
limit: int = 25,
) -> list[Commit]:
...
async def resolve_committish(
self,
ctx: PluginContext,
credentials: dict[str, str],
committish: str,
) -> Commit:
...
async def compare(
self,
ctx: PluginContext,
credentials: dict[str, str],
base: str,
head: str,
) -> CompareResult:
...
async def trigger_deployment(
self,
ctx: PluginContext,
credentials: dict[str, str],
ref_or_sha: str,
inputs: dict[str, str] | None = None,
) -> DeploymentRun:
...
async def get_deployment_status(
self,
ctx: PluginContext,
credentials: dict[str, str],
run_id: str,
) -> DeploymentRun:
...
Required methods¶
list_refs— enumerate branches / tags / the default ref, optionally filtered bykindand aquerysubstring.list_commits— list commits reachable fromref, newest first.resolve_committish— hydrate a single branch / tag / SHA into aCommit.compare— return aCompareResultforbase..head(ahead / behind counts, commits, diffstat).trigger_deployment— dispatch a CI workflow / pipeline run forref_or_shawith optionalinputs, returning aDeploymentRun.get_deployment_status— poll a previously triggered run byrun_id.
Optional methods¶
These default to returning 'unknown' or None, or raising
NotImplementedError; implement only what the remote supports.
get_check_status— aggregate CI check-runs into a singleCheckStatus(pass/fail/warn/unknown) for a ref. Powers the release-train green/red dot.create_tag/create_release— mint an annotated tag and a release on the remote; required only for the Promote flow.list_workflows— list CI workflow files so the UI can populate a workflow dropdown when an operator wires up the per-environment dispatch edge.list_recent_deployments— return the most recent deployments per environment for the resync flow that backfillsReleasenodes andDEPLOYED_TOedges when webhook delivery has lapsed. Capabilities that set thesupports_deployment_synchint must implement this; each returnedRemoteDeploymentmust carry a stableexternal_run_idso the host can dedupe. Status values use the host's canonicalDeploymentEventStatusvocabulary (pending,in_progress,success,failed,rolled_back). To attribute the deployer to an Imbi user, populatecreator(the remote login, for display) andcreator_subject(the remote's stable identity subject — e.g. the numeric GitHub user id); the host resolves the latter through the Integration's identity capability. When the deployment targets a tagged release, populaterelease_noteswith the release's notes body (e.g. a GitHub release's "What's Changed" markdown); the host persists it as theReleasenode's notes, distinct from the shortdescriptiondeploy note.get_environment_state— report which deployment is active per environment, as the provider sees it. The currency counterpart tolist_recent_deployments, which only answers "what happened recently": the newest attempt may have failed, still be in flight, or have been superseded, leaving an older deployment live. Each returnedEnvironmentDeploymentStatecarriesactive,latest(the newest attempt of any status), and anactive_resolutionoffound/none/unknown/error.unknown(a bounded scan stopped before exhausting the result set) anderror(the provider call failed) both mean "retain the pointer", and neither may be reported asnone— clearing a correct pointer is the failure mode this method exists to prevent. Optional even for capabilities that setsupports_deployment_sync; hosts probe by catchingNotImplementedError.get_release_notes— return the remote release's notes body for a given tag. The tag-keyed counterpart tolist_recent_deployments'release_notesfield: it lets the host enrich aReleasenode's notes on paths that only know the tag — a webhook that created the release from a deployment event (which carries no body), or a resync whose deploymentrefwas a raw SHA. Best-effort: capabilities without a release concept, or that cannot resolve one for the tag, returnNone(the default) so the host never fails a write on a missing or unreadable release.
Hints¶
supports_deployment_sync— the capability implementslist_recent_deploymentsfor the resync flow.cacheable— the host may cache reads from this capability.
Link write-back¶
When a deployment call creates, renames, or discovers the project's
canonical repository URL, set
ctx.link_writeback so the host
self-heals the stored project link. See
Plugin Context.
API reference¶
DeploymentCapability ¶
Bases: CapabilityHandler
Act on a repo: enumerate refs/commits, compare them, create tags/releases, and trigger CI workflow runs.
create_deployment_artifact
async
¶
create_deployment_artifact(
ctx: PluginContext,
credentials: dict[str, str],
ref: str,
version: str,
inputs: dict[str, str] | None = None,
) -> ArtifactRun
Start the build that produces the deployable artifact.
The step :meth:trigger_deployment currently assumes has already
happened. ref is the committish to build from and version
the release identity the build is expected to publish under -- the
remote build is what cuts the tag and pushes any version-bump
commits, so neither exists yet when this returns.
Optional and deliberately separate from
:meth:trigger_deployment: the two are distinct stages with host
handling between them, not two halves of one call. Returns as
soon as the build is accepted; the host observes completion via
:meth:get_artifact_run_status (or a webhook) and only then
proceeds to deploy.
Capabilities whose remote has no build concept raise
:class:NotImplementedError.
Source code in libraries/common/src/imbi/common/plugins/base.py
create_release
async
¶
create_release(
ctx: PluginContext,
credentials: dict[str, str],
tag: str,
name: str,
body_markdown: str,
prerelease: bool = False,
) -> ReleaseInfo
Create a release on the remote (e.g. a GitHub Release).
Optional — paired with :meth:create_tag. Capabilities without a
release concept raise :class:NotImplementedError.
Source code in libraries/common/src/imbi/common/plugins/base.py
create_tag
async
¶
create_tag(
ctx: PluginContext,
credentials: dict[str, str],
sha: str,
tag: str,
message: str,
) -> RefInfo
Create an annotated tag on the remote.
Optional — only required for the Promote flow. Capabilities that
cannot mint tags raise :class:NotImplementedError; the host
surfaces the error to the caller.
Source code in libraries/common/src/imbi/common/plugins/base.py
diff_commit_notes
async
¶
diff_commit_notes(
ctx: PluginContext,
credentials: dict[str, str],
namespace: str,
before: str,
after: str,
) -> dict[str, str | None]
Diff a notes ref between two of its own commits.
before and after are commits of the notes ref (what a
push event to refs/notes/<namespace> reports), not annotated
commits. Returns a map of annotated commit SHA (full, fan-out
subtrees flattened) to the note's new body -- None for a
note the push removed. An all-zero before means the ref was
just created; implementations return every note in after.
Optional -- same contract as :meth:get_commit_note.
Source code in libraries/common/src/imbi/common/plugins/base.py
get_artifact_run_status
async
¶
get_artifact_run_status(
ctx: PluginContext,
credentials: dict[str, str],
run_id: str,
) -> ArtifactRun
Report a build run's status.
The :class:ArtifactRun counterpart to
:meth:get_deployment_status; run_id is what
:meth:create_deployment_artifact returned, resolved against the
remote's build endpoint rather than its deployment one.
Optional -- paired with :meth:create_deployment_artifact.
Source code in libraries/common/src/imbi/common/plugins/base.py
get_check_status
async
¶
get_check_status(
ctx: PluginContext,
credentials: dict[str, str],
committish: str,
) -> CheckStatus
Aggregate CI check-runs status for a ref / SHA / tag.
Optional — used by the release-train read path to surface a
green/red dot per env's currently-deployed version. Capabilities
without a CI concept return 'unknown' (the default).
Source code in libraries/common/src/imbi/common/plugins/base.py
get_commit_note
async
¶
get_commit_note(
ctx: PluginContext,
credentials: dict[str, str],
namespace: str,
committish: str,
) -> str | None
Return the git note on committish in namespace.
namespace names the notes ref without its refs/notes/
prefix (e.g. imbi-drift for refs/notes/imbi-drift).
committish may be short; the notes tree is keyed by the full
SHA, so implementations resolve it first. Returns the note's
raw body, or None when the ref does not exist or carries no
note for the commit -- both mean "no note", which is a real
answer, not a failure.
Optional -- capabilities without a git-notes concept raise
:class:NotImplementedError so hosts can tell "no note" apart
from "cannot answer".
Source code in libraries/common/src/imbi/common/plugins/base.py
get_environment_state
async
¶
get_environment_state(
ctx: PluginContext,
credentials: dict[str, str],
environments: list[str],
) -> list[EnvironmentDeploymentState]
Report which deployment is active per environment.
The currency counterpart to :meth:list_recent_deployments: that
method answers "what happened recently", this one answers "what is
serving the environment now", which an attempt list cannot -- the
newest attempt may have failed, still be in flight, or have been
superseded, leaving an older deployment as the live one.
Optional even for capabilities that advertise
supports_deployment_sync: hosts probe by calling it and
treating :class:NotImplementedError as "this provider cannot
report currency", falling back to the attempt list alone. Returns
one :class:EnvironmentDeploymentState per requested environment,
never dropping one, because the host has to distinguish "not
answered" from "nothing deployed".
An environment the remote does not recognise resolves unknown,
NOT none. The host passes its own environment slugs through
unmapped, so a slug the provider has never heard of is
indistinguishable from one with no deployments -- and none
authorizes the host to clear its current-release pointer. Only
positive evidence earns it: deployments read, none of them
serving.
Implementations walk the provider newest-first under a bounded
cap. Reaching the cap without an answer is unknown; a read
that failed -- the listing errored, or a row's status would not
load -- is error. Both retain the host's pointer, so the
distinction is not about safety but about diagnosis: reporting a
provider outage as unknown files it as a scan-limit result
and hides it from the operator, and only error reaches
summary.errors. Neither may be reported as none. See
:class:EnvironmentDeploymentState for the invariants each
resolution carries.
Source code in libraries/common/src/imbi/common/plugins/base.py
get_release
async
¶
Return the remote release for tag, metadata included.
The richer counterpart to :meth:get_release_notes: same lookup,
but it also carries who cut the release on the remote, its title,
and its URL, so the host can attribute a release it only observed
to a person instead of to the process that recorded it.
Optional. Capabilities without a release concept, or that cannot
resolve one for tag, return None (the default); hosts fall
back to :meth:get_release_notes for the body alone.
Source code in libraries/common/src/imbi/common/plugins/base.py
get_release_notes
async
¶
Return the remote release's notes body for tag.
Optional -- lets the host enrich a Release node's notes with
the remote release body (e.g. a GitHub release's "What's Changed"
markdown) on paths that only know the tag, such as a webhook that
created the release from a deployment event (which carries no
body) or a resync whose deployment ref was a raw SHA. This is
the tag-keyed counterpart to the release_notes field
:meth:list_recent_deployments populates from a deployment's ref.
Best-effort: capabilities without a release concept, or that
cannot resolve one for tag, return None (the default) so
the host never fails a write on a missing or unreadable release.
Source code in libraries/common/src/imbi/common/plugins/base.py
list_commit_notes
async
¶
list_commit_notes(
ctx: PluginContext,
credentials: dict[str, str],
namespace: str,
skip_shas: Collection[str] = (),
) -> NotesListing
Return every note in namespace, keyed by annotated SHA.
The whole-ref counterpart to :meth:diff_commit_notes, for a
host backfilling notes it never saw a push for. Keys are full
SHAs with fan-out subtrees flattened, exactly as
:meth:diff_commit_notes returns them; a missing ref is an empty
listing, not an error, because "no notes" is a real answer.
skip_shas names annotated commits the host already holds an
answer for. Enumerating the ref is a call or two; reading the
bodies is one per note, so a host repairing a gap would pay for
the whole history to learn about the handful of notes it is
missing. Implementations must still enumerate the ref -- the
skip applies to reading bodies, and skipped notes are absent from
the map rather than present with a None body, which means
"removed".
complete says whether every note the ref holds is either in
the map or was skipped on request. It has to be reported rather
than inferred: a host cannot tell a ref with three notes from a
ref with four whose fourth could not be read, and treating the
second as the whole truth would record a partial backfill as a
finished one.
Optional -- same contract as :meth:get_commit_note.
Source code in libraries/common/src/imbi/common/plugins/base.py
list_recent_deployments
async
¶
list_recent_deployments(
ctx: PluginContext,
credentials: dict[str, str],
environments: list[str],
limit: int = 1,
) -> list[RemoteDeployment]
Return the most recent limit deployments per environment.
Optional -- powers the deployment resync flow that backfills
Release nodes and DEPLOYED_TO edges when webhook delivery
has lapsed. Capabilities that advertise the
supports_deployment_sync hint MUST implement this; others
raise :class:NotImplementedError (the host treats that as
"skip resync").
environments is the remote-facing list of environment names
the host wants populated, in the project's preferred order.
Capabilities should ignore environments their remote does not know
about (rather than raising) so a partial resync still succeeds.
Returned events MUST carry a stable external_run_id so the
host can dedupe. When a deployment targets a tagged release,
populate release_notes with the release's notes body (distinct
from the short description deploy note) so the host can persist
it as the Release node's notes.
Source code in libraries/common/src/imbi/common/plugins/base.py
list_workflows
async
¶
List CI workflow files defined in the project's remote repo.
Optional — used by the UI to populate a workflow dropdown when an
operator configures assignment env_payloads. Capabilities
without a workflow concept raise :class:NotImplementedError.
Source code in libraries/common/src/imbi/common/plugins/base.py
Ref ¶
Bases: BaseModel
A branch, tag, or default-ref pointer on a deployable repo.
Commit ¶
Bases: BaseModel
A commit on a deployable repo, hydrated for UI display.
CompareResult ¶
Bases: BaseModel
Result of comparing two commit-ish refs (base..head).
RefInfo ¶
Bases: BaseModel
Metadata returned after creating a ref (e.g. an annotated tag).
ReleaseInfo ¶
Bases: BaseModel
Metadata returned after creating a release on the remote.
WorkflowFile ¶
Bases: BaseModel
A CI workflow file discoverable in the project's remote repo.
Returned by :meth:DeploymentCapability.list_workflows so the UI can
populate a workflow dropdown when an operator wires up the
per-environment dispatch edge. id is the remote's stable
identifier (e.g. the GitHub workflow id); path is the repo-
relative file path; name is the human label from the workflow
file's name: field.
DeploymentRun ¶
Bases: BaseModel
A workflow / pipeline run triggered by trigger_deployment.
RemoteDeployment ¶
Bases: BaseModel
A deployment observed on the remote for resync.
Returned by :meth:DeploymentCapability.list_recent_deployments so
the host can backfill Release nodes and DEPLOYED_TO edges when
webhook delivery has lapsed or when bringing a project online for
the first time. environment is the remote's environment name as
reported (callers map it to the project's local environment slug).
sha is the commit the deployment was created against;
ref is the human-facing label the deploy was triggered with
(branch / tag / SHA prefix) and may be None for older deploys.
external_run_id MUST be a stable identifier (e.g. the GitHub
deployment id) so the host can dedupe re-runs of resync without
appending duplicate events.
EnvironmentDeploymentState ¶
Bases: BaseModel
What the remote says is deployed to one environment now.
Returned by :meth:DeploymentCapability.get_environment_state so the
host can reconcile its "current release" pointer against the provider
instead of inferring currency from the newest deployment attempt it
happens to have observed.
active_resolution is what makes the answer actionable, and its
four outcomes are deliberately distinct:
found--activeis notNone: the provider reports this deployment as the one serving the environment.none-- the provider's result set was exhausted and no deployment qualifies;activeisNone. The host may clear its pointer.unknown-- the bounded scan stopped before exhausting the result set, so an older active deployment may exist unseen;activeisNonebut the host MUST retain its pointer.error-- the provider call failed. Also "retain the pointer", but it needs a different operational response thanunknown: "we scanned the cap" and "the remote returned 503" are not the same problem.
latest -- the newest attempt of any status -- may be populated
for every outcome, error included when the failure happened after
the listing was read. It is never a substitute for active: an
in-flight or failed attempt is activity, not currency.