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.
import typing
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: typing.Literal['default', 'branch', 'tag', 'all'] = '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.get_release— return aRemoteReleasefor a tag: the notes body plus the title, the URL, and who cut the release on the remote (author, andauthor_subjectfor identity resolution). The host uses it to attribute a release to a person. ReturnNone(the default) when the remote has no release for the tag. The host then falls back toget_release_notes.get_file— return the text of the file atpathonref, orNonewhen the file does not exist. The default raisesNotImplementedError. The host uses it to read agent plugin marketplace files from a repository.public_reader(url)(class method) — return aRepositoryReaderthat reads the public repository aturlwith no Integration and no credentials, orNone(the default). The host uses it when no Integration of the organization can read a marketplace repository. The GitHub plugin returns a reader forgithub.comrepositories.
Release builds¶
The promote and release-cut flows can build the artifact before they
deploy it. These two methods are optional and work as a pair. The
default implementations raise NotImplementedError.
create_deployment_artifact— start the remote build that makes the deployable artifact forref, under the release identityversion. The remote build cuts the tag and pushes version-bump commits, so these do not exist yet when the method returns. Return anArtifactRunas soon as the remote accepts the build.run_idcan beNonewhen the remote does not report the run it started. That is a success, not an error.get_artifact_run_status— report the status of a build by therun_idthatcreate_deployment_artifactreturned. Resolve it against the remote's build endpoint, not its deployment endpoint.
ArtifactRun and DeploymentRun are separate models on purpose. The
two IDs come from different identifier spaces on the same remote (for
GitHub, a workflow run ID and a Deployment ID).
Git notes¶
The host reads drift verdicts from git notes (the imbi-drift notes
ref). Three optional methods give it access. Each default raises
NotImplementedError, so the host can tell "no note" from "cannot
answer". namespace is the notes ref name without the refs/notes/
prefix.
get_commit_note— return the note body on one commit, orNonewhen the ref or the note does not exist. The commit can be a short SHA; resolve it to the full SHA first.list_commit_notes— return every note in the ref as aNotesListing(notes, keyed by full SHA, andcomplete). Skip reading the bodies of the SHAs inskip_shas, but still enumerate the ref. Setcomplete=Falsewhen a note could not be read. A missing ref is an empty listing, not an error.diff_commit_notes— return the notes that changed between two commits of the notes ref (what a push torefs/notes/<namespace>reports). ANonevalue means the push removed that note. An all-zerobeforemeans the ref is new; return every note inafter.
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_file
async
¶
Return the text of the file path at ref.
Returns None when the file does not exist at ref.
Optional -- capabilities that cannot read files raise
:class:NotImplementedError.
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
public_reader
classmethod
¶
Return a reader of the public repository at url.
The reader needs no Integration and no credentials. The host
uses it when no Integration of the organization can read the
repository. None: the plugin cannot read url without
credentials (the default).
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.
ArtifactRun ¶
Bases: BaseModel
A build/release run started by create_deployment_artifact.
Deliberately not a :class:DeploymentRun: the two carry different
identifier spaces on the same remote (a GitHub workflow run id here
versus a GitHub Deployment id there), resolved through different
endpoints. Keeping them apart is what lets the host poll each with
the right call instead of guessing from the id alone.
run_id is None when the remote accepted the request but did
not report which run it started -- GitHub's
workflow_dispatch answers 204 No Content on API versions
before 2026-03-10. That is a success, not an error: the artifact
build is running. Hosts that need to watch it must resolve the id
separately (e.g. by listing recent runs filtered on the dispatch
time) rather than treating the dispatch as failed.
RemoteRelease ¶
Bases: BaseModel
A release observed on the remote, keyed by tag.
Returned by :meth:DeploymentCapability.get_release so the host can
attribute a release to whoever cut it on the remote rather than to
whichever Imbi process happened to record the node. author is the
remote's own handle (a GitHub login); author_subject is the
identity-plugin subject (GitHub's numeric user id) the host resolves
through its identity connections to reach an Imbi user.
NotesListing ¶
Bases: NamedTuple
Every git note on one ref, and whether that is all of them.
notes maps annotated full SHA to note body. complete is
False when the ref holds notes this listing could not read, so a
caller persisting the result knows not to call the job done.