Compose deployment
The supported Infrahub Sync deployment is one host running deploy/compose/. It
owns the Sync API, the Sync worker, a Prefect server, PostgreSQL, and
S3-compatible object storage. A host needs Docker Compose 2.17.3 or later and
nothing else — no product checkout, no Python, no uv.
Private candidates are available as release attachments. Start with Install Infrahub Sync for release access and the guides pinned for Candidate 4.
Start from the Compose quickstart, which owns release
verification, image loading, init, and the first start. This page owns the
configuration and reviewed-run procedure after that prerequisite.
Related operator pages
Day 2 operations · Compose operator troubleshooting · Supported platforms and limits
The extracted bundle includes OPERATING.md with local operating instructions
for that bundle. Follow the release download and checksum steps before using it.
Release prerequisite
Use the Compose quickstart to verify and extract the
release, load its image, initialize the deployment, and perform the first
start. Continue here only after it reports READY.
The checksum is a claim about the archive as shipped and about nothing
afterwards: preparing a deployment writes operator.env and edits the declared
configuration, and both leave the extracted tree different from the archive.
The archive names the image it was qualified against. That record is
image.bind, generated by the release and shipped inside the bundle, and there
is no setting anywhere that names a different one:
cat image.bind
It holds the qualified platform and the same image in the three immutable forms a host can hold it under:
INFRAHUB_SYNC_IMAGE_PLATFORM=linux/amd64
INFRAHUB_SYNC_IMAGE_INDEX=<index name>@sha256:<64 hex>
INFRAHUB_SYNC_IMAGE_MANIFEST=sha256:<64 hex>
INFRAHUB_SYNC_IMAGE_CONFIG=sha256:<64 hex>
Which form a host holds is decided by the image store its Docker runs. The
containerd image store — the default from Docker Engine 29 — identifies a loaded
archive by the digest of a manifest it synthesizes at load time, which is the
MANIFEST line; the classic store identifies it by the configuration digest,
which is the CONFIG line. The INDEX line is the exported OCI index, which a
host that kept the original export already holds.
The image is named by digest, never by a tag: a tag can be re-pointed between the qualification that trusted it and the run that uses it, so the entry point refuses one before it creates anything.
Load that image onto the host before the first start, from the
image-linux-amd64.tar the release publishes beside the bundle. The deployment
settles which form it got by itself: the index first, then the manifest digest,
then the configuration digest, asking only this engine. No registry is consulted
— what a registry holds says nothing about what this host can run, and a private
candidate is loaded rather than pulled.
Confirming that what you loaded is what the record names is one command, and the identifier it prints is one of the record's three forms:
docker image inspect --format '{{.Id}}' <reference>
What the bundle contains
| Path | What it is |
|---|---|
compose.yaml | The topology: every service, network, volume, and label. |
infrahub-sync-compose | The lifecycle entry point. The only thing that talks to Docker. |
defaults.conf | Shipped non-secret defaults. Tracked; safe to read. |
configuration/qualification.yaml | An example declared package, for you to register explicitly. Nothing loads it. |
bootstrap/databases.sh | The read-only script that creates the two databases and their owners. |
OPERATING.md | This procedure, shipped inside the archive for a host that has only the archive. |
skills/README.md | Discovery and manual installation guidance for the optional agent skills. |
skills/infrahub-sync-deployment/SKILL.md | Routes deployment diagnosis and saved-plan review to the human procedure. |
skills/infrahub-sync-configuration/SKILL.md | Routes configuration drafting and registered-version validation without registry writes. |
operator.env | Your credentials. Generated by init, Git-ignored, never printed. |
secrets/postgres-admin-password | The one file-mounted credential. Generated by init. |
image.bind | The image this bundle was qualified against, generated by the release. Not yours to edit. |
.instance | This deployment's generated identity and selected image. Non-secret; the identity is the ownership label. |
The skills are unreleased source content and are not included in Candidate 4
(3.0.0a4). A later bundle built from a revision containing these files can be
installed manually by following skills/README.md; no bundle installs or
updates an agent skill automatically. The operator procedures on this page and
in OPERATING.md remain authoritative, and a skill grants no permission to
change a deployment, registry, source, or destination.
The API and the worker share no mount, no volume, and no scratch directory. Run state travels between them through PostgreSQL and the object store, which is why the worker needs no configuration mount and why replacing it loses nothing. PostgreSQL and the object store hold the only volumes that survive a restart.
Configuration prerequisites
That writes .instance, secrets/postgres-admin-password, and operator.env
with generated passwords for the two database owner roles, the object store, and
one API principal. Nothing in operator.env is yours to supply before the first
start.
init needs no Docker. It reads image.bind, checks its grammar, and copies the
index reference into .instance as plain text, which is what gives every later
command an image name to interpolate — so init followed by status, logs,
stop, restart or reset works before the image has been loaded at all. It
resolves nothing and asks no engine anything.
Nothing else is needed to start. The deployment comes up with an empty
configuration registry: which package it runs is a decision you make afterwards,
by registering one through the Sync API. configuration/qualification.yaml in
the bundle is an example of that package's shape and is never loaded for you.
The credentials a registered package references belong in operator.env before
the run that needs them, and init leaves each one commented:
INFRAHUB_API_TOKEN=<your Infrahub token>
Credentials in a package are references, never values:
token:
$credential: infrahub-token
credentials:
infrahub-token:
provider: env
identifier: INFRAHUB_API_TOKEN
A container reads its environment once, at start. After changing a value in
operator.env, run ./infrahub-sync-compose start again — that recreates the
services whose environment changed. restart replaces the processes inside
containers that keep the environment they were created with.
Reading from NetBox or Nautobot
The image installs the bundled NetBox and Nautobot source adapters, so a package
may declare either as its source. init leaves both token names commented in
operator.env; remove the leading # only from the token entry your package
names.
NETBOX_TOKEN=<your NetBox token>
# NAUTOBOT_TOKEN=<your Nautobot token>
configuration:
source:
name: netbox
settings:
url: "http://netbox.example.net:8080"
token:
$credential: netbox-token
credentials:
netbox-token:
provider: env
identifier: NETBOX_TOKEN
Nautobot uses the same shape with nautobot, nautobot-token and
NAUTOBOT_TOKEN.
- Name a URL a container can reach. The worker resolves it from inside the
Compose network, where
localhostis the worker itself. - The declared
urlis the one used.NETBOX_ADDRESS,NETBOX_URL,NAUTOBOT_ADDRESSandNAUTOBOT_URLexported in your shell are ignored: a registered run reads what its package declares. - Both tokens are optional. A deployment that reads from neither source starts normally, and a package naming one source does not need the other's token. When a package does select a source, a missing or empty token fails that run and the message names the environment variable, never its value.
- Start again after changing either value. A container reads its environment
once, at start, so
./infrahub-sync-compose startrecreates the worker;restartkeeps the environment it already has.
Only sync-worker receives these tokens. Registration and the default
validation judge declared content without resolving a source secret, so the API
and the bootstrap job never hold one.
That both adapters install and import is a packaging property of this release. It is not a statement that any particular NetBox or Nautobot server version has been qualified against it.
After the first start
start runs preflight itself, so the separate call is optional. It is a
diagnostic rather than a read-only one: it may replace the image recorded in
.instance with whichever of the record's three forms this engine resolved. It
starts no service, changes nothing in operator.env or secrets/, leaves the
instance identity alone, and reaches no source or destination.
preflight refuses before anything is created, and every refusal is one family
name followed by a fixed sentence. It never renders a credential value.
| Family | What to do |
|---|---|
compose-unreadable | Docker Compose reported no version this can compare. Check the installation. |
compose-too-old | Install Docker Compose 2.17.3 or later. |
path-unwritable, path-unreadable, path-missing | The bundle directory, secrets/, or the generated .instance is not usable by this user. A refused state write leaves the previous .instance exactly as it was. |
credentials-missing | The named settings in operator.env are empty. |
image-binding-missing | image.bind is absent or unreadable, so this bundle names no image. Extract the archive again. |
image-binding-invalid | image.bind is missing one of its four settings, names a platform this bundle is not qualified on, or names something that is not an immutable digest. |
image-binding-mismatch | .instance names an image image.bind does not. Run init. |
image-not-immutable | The selected reference carries a malformed digest. |
image-unresolvable | This engine holds none of the three identities image.bind names. Load image-linux-amd64.tar on this host; no registry is consulted. |
image-platform-unqualified | The image this engine resolved is another architecture. It is not a fallback: load the qualified one. |
port-occupied | Something else already holds one of the required loopback binds. Preflight asks the engine for the bind rather than reading container names, so a foreign container published on any address and a host process that is not a container are both found. |
port-unprovable | The disposable bind probe could not run at all, so the port was neither proved free nor proved held. Check that the Sync image runs on this host. |
docker-unavailable | Docker could not enumerate or inspect this instance. This is not evidence that the deployment is stopped; restore Docker access and repeat the command. |
start runs preflight, brings the deployment up, and returns only once the
Sync API reports a worker that has registered. It is safe to repeat: bootstrap
converges the two databases, their owner roles, the product schema, the artifact
bucket, the Prefect process work pool, and the installed deployment, and a second
run creates none of them twice. It registers nothing, so a repeat leaves every
configuration you registered exactly as it was.
READY describes this deployment's own dependencies and a live worker, and
nothing about a configuration. A deployment reaches READY with an empty
registry, no credentials set, and no reachable destination; whether a registered
package is valid is answered by configs validate, and whether its destination
answers is answered by the first run against it.
Both published surfaces bind to loopback by default:
| Surface | Address |
|---|---|
| Sync API | http://127.0.0.1:8000 |
| Prefect UI and API | http://127.0.0.1:4200 |
External exposure and TLS termination are not part of this topology.
Prepare the destination schema
The existing-Infrahub tutorials use a small destination schema with two kinds,
TestingSite and TestingDevice:
version: '1.0'
nodes:
- name: Site
namespace: Testing
human_friendly_id: [name__value]
uniqueness_constraints: [[name__value]]
attributes:
- name: name
kind: Text
optional: false
- name: Device
namespace: Testing
human_friendly_id: [name__value]
uniqueness_constraints: [[name__value]]
attributes:
- name: name
kind: Text
optional: false
- name: model
kind: Text
optional: false
- name: serial
kind: Text
optional: false
relationships:
- name: site
peer: TestingSite
kind: Attribute
cardinality: one
optional: false
Save it as schema.yml, then use the Infrahub SDK CLI on the Docker host to
load it. Install the CLI outside the deployment containers:
python3 -m venv .schema-tools
.schema-tools/bin/python -m pip install 'infrahub-sdk[ctl]'
.schema-tools/bin/infrahubctl version
Set the destination address and token for one command only. Replace the address
with the URL in destination.settings.url; do not source operator.env.
(
export INFRAHUB_ADDRESS="https://infrahub.example.invalid"
export INFRAHUB_API_TOKEN="$(sed -n 's/^INFRAHUB_API_TOKEN=//p' operator.env)"
.schema-tools/bin/infrahubctl schema load schema.yml --branch BRANCH_NAME
)
BRANCH_NAME must already exist. List or, when needed, create it with the
same scoped environment:
(
export INFRAHUB_ADDRESS="https://infrahub.example.invalid"
export INFRAHUB_API_TOKEN="$(sed -n 's/^INFRAHUB_API_TOKEN=//p' operator.env)"
.schema-tools/bin/infrahubctl branch list
# Only if you want to create a new branch:
.schema-tools/bin/infrahubctl branch create BRANCH_NAME
)
Use this branch for the later diff and apply. Omit --branch from schema
load to target Infrahub's default branch. The destination schema is your
Infrahub instance's responsibility; this deployment's CLI does not load it.
Use it
cli runs the shipped CLI against this deployment, in a container of the same
image, on the deployment's own network:
./infrahub-sync-compose cli configs list
There is nothing to authenticate by hand. init generated one value and wrote it
to both settings that name it — INFRAHUB_SYNC_SERVICE_BEARER_TOKENS, the
principal the Sync API accepts, and INFRAHUB_SYNC_API_TOKEN, what cli
presents. They are one credential: if you change one, change the other to the
same value.
An operator.env generated by an earlier alpha has no INFRAHUB_SYNC_API_TOKEN
line at all, and nothing adds one: init leaves an existing file exactly as it
found it, and this alpha migrates no state in place. Every cli call against
such a bundle fails to authenticate. Add the setting by hand and give it the
token already inside that same file's INFRAHUB_SYNC_SERVICE_BEARER_TOKENS,
which is the principal the deployment authorizes; nothing rotates it for you,
and any other value leaves the CLI unauthenticated again. Never source
operator.env to read it: that file holds every other credential of the
deployment.
Like preflight, cli resolves the image before it runs anything, so it too
may replace the image recorded in .instance with whichever of the record's three
forms this engine resolved. It changes nothing else in the bundle.
Each call is one container: nothing is published, no service is started as a
side effect, no volume is kept, and the container is removed when the command
ends. cli --help displays help for the CLI, and everything after cli is
passed through as given — an argument holding spaces stays one argument.
Prepare a configuration
The NetBox and
Nautobot tutorials include schema and
package preparation, registration, validation, and a first preview. Continue
here for the full operator sequence and lifecycle commands. After changing
operator.env, run ./infrahub-sync-compose start so services receive the new
environment.
Source credentials are optional for startup but required for a run that uses
them.
Registering a package
A package is a file on your machine, and the CLI runs in a container. --package
bridges that for one call: the file's bytes are copied into a private directory,
mounted read-only at /input/package.yaml, and removed when the command ends.
Your own file is never mounted and never modified, so its permissions are left
alone.
./infrahub-sync-compose cli --package ./package.yml -- \
configs register /input/package.yaml --reason 'register my configuration'
Two refusals belong to --package alone: cli-usage when it is given no file,
and package-unusable when the named path is not a readable regular file. Both
name the path and never its content.
-- separates options for the wrapper from options for the CLI. After
registration the declared content lives in PostgreSQL: no host file is mounted
by the API or the worker, and the file you registered from is not needed again.
Every later command names the configuration by its identifier and version.
Two things a package declares are resolved elsewhere:
- Its URLs are resolved inside a container.
localhostthere is the container itself. Name an address the Compose network can reach. - Its credentials are references. The value lives in
operator.envand is resolved by the service that needs it, so no secret is registered, stored or echoed. Add the credential before the run that needs it and runstartagain to recreate the services whose environment changed.
The operator sequence
./infrahub-sync-compose cli configs list
# Add destination/source credentials to operator.env before planning; recreate affected services
# using start after changed environment; restart alone retains the old container environment.
./infrahub-sync-compose start
./infrahub-sync-compose cli --package ./package.yml -- configs register /input/package.yaml --reason 'register my configuration'
./infrahub-sync-compose cli configs show CONFIG_ID
./infrahub-sync-compose cli configs versions CONFIG_ID
./infrahub-sync-compose cli configs validate CONFIG_ID 1
./infrahub-sync-compose cli diff --config-id CONFIG_ID --version 1 --branch main --reason 'review initial sync'
./infrahub-sync-compose cli runs plan RUN_ID --detail
./infrahub-sync-compose cli apply RUN_ID --expected-checksum CHECKSUM --branch main --reason 'apply reviewed initial sync'
./infrahub-sync-compose cli runs show RUN_ID
./infrahub-sync-compose cli runs results RUN_ID
./infrahub-sync-compose cli diff --config-id CONFIG_ID --version 1 --branch main --reason 'verify unchanged source'
# Versioning is explicit, and never rewrites a registered version:
./infrahub-sync-compose cli --package ./edited-package.yml -- configs version CONFIG_ID /input/package.yaml --reason 'register edited configuration'
CONFIG_ID, RUN_ID and CHECKSUM are results the previous commands printed.
runs show reports the run's phase, outcome and Prefect correlation; runs results prints what the service recorded for it as JSON.
On a new deployment, configs list prints no command output and exits zero;
the Container … Creating/Created lines are Compose progress. The plan output
does not repeat its branch: apply must use the same --branch supplied to
diff. This release writes saved-plan format 3; it can review format 2 but
refuses to apply it, and does not support older formats.
A null optional cardinality-one relationship in an update does not clear the destination field. The plan format cannot distinguish an absent relationship from an intended clear, so it is omitted and the worker logs a warning.
runs show keeps operation: plan; phase: applied and
execution_state: completed confirm completion. Result counts come from the
plan, not executed writes. Deletes are recorded but never executed; use
runs plan RUN_ID --detail to identify each (not executed) delete. With an
unchanged, fully applied mapping, the next plan has no creates or updates except
for any recurring recorded delete.
If the final output from apply still shows execution_state: running, run
runs show again. It settles within seconds; confirm phase: applied and
execution_state: completed before treating the apply as finished.
Over HTTP or from Python
The same deployment answers directly. /status needs no credential:
export INFRAHUB_SYNC_API_URL=http://127.0.0.1:8000
curl -sS "$INFRAHUB_SYNC_API_URL/status"
An authenticated call is what cli is for — ./infrahub-sync-compose cli configs list reads the same route. For a route the CLI has no command for, put the
header in a private curl configuration rather than on the command line, where
the value would be visible to every process on the host while the request runs:
umask 077
CURL_CONFIG=$(mktemp)
# `printf` is a shell builtin and `sed` never sees the value, so the token
# reaches no process argument list. Run this from the bundle directory.
printf 'header = "Authorization: Bearer %s"\n' \
"$(sed -n 's/^INFRAHUB_SYNC_SERVICE_BEARER_TOKENS=.*"token": "\([^"]*\)".*/\1/p' operator.env)" \
> "$CURL_CONFIG"
curl -sS --config "$CURL_CONFIG" "$INFRAHUB_SYNC_API_URL/configs"
rm -f "$CURL_CONFIG"
Never source operator.env: it holds every other credential of the
deployment.
From Python:
from infrahub_sync.client import SyncClient
with SyncClient("http://127.0.0.1:8000", "<the generated principal token>") as client:
print(client.get_status().worker.state)
Plan verification details
A managed write is admitted once and approved against the checksum of the plan you reviewed. Nothing is written at the destination until the apply names that exact checksum and confirms.
The operator sequence above is the authoritative command sequence for planning, reviewing, applying, and reading a run. The following API verification is optional and does not create another plan or apply sequence.
An independent verification of a retained plan is the one step here with no CLI command. It is a direct authenticated call, so it creates its own private curl configuration and removes it afterwards:
umask 077
CURL_CONFIG=$(mktemp)
# `printf` is a shell builtin and `sed` never sees the value, so the token
# reaches no process argument list. Run this from the bundle directory.
printf 'header = "Authorization: Bearer %s"\n' \
"$(sed -n 's/^INFRAHUB_SYNC_SERVICE_BEARER_TOKENS=.*"token": "\([^"]*\)".*/\1/p' operator.env)" \
> "$CURL_CONFIG"
curl -sS -X POST --config "$CURL_CONFIG" \
-H "Idempotency-Key: $(uuidgen)" -H 'Content-Type: application/json' \
-d '{"reason": "second opinion before applying"}' \
"$INFRAHUB_SYNC_API_URL/runs/RUN_ID/verify"
rm -f "$CURL_CONFIG"
Status
./infrahub-sync-compose status
| State | Exit | What it means |
|---|---|---|
READY | 0 | Dependencies answer, the API answers, and a registered worker is sending heartbeats. |
| Refusal | 1 | The command did not run; follow its message, such as no-instance before init. |
DEGRADED | 3 | Something owned exists, but that is not all true. A stopped or hung worker lands here. |
STOPPED | 4 | No container of this instance is running. |
Container state establishes absence and nothing else. A paused worker keeps
looking running to Docker; the deployment reports DEGRADED about thirty
seconds after its heartbeat stops, because readiness comes from what the
Sync API reports about the Prefect registry rather than from a process being up.
A worker that has been stopped outright reaches DEGRADED the same way, and for
the same reason it is not STOPPED: the API still answers, submissions are
still accepted, and they will sit unclaimed until a worker comes back. Start the
worker again with ./infrahub-sync-compose restart; it verifies ownership,
replaces the API and worker processes, and waits for READY without changing
any durable record.
Logs
./infrahub-sync-compose logs
./infrahub-sync-compose logs sync-worker
The tail is bounded (200 lines per service by default; set
INFRAHUB_SYNC_LOG_LINES). Credentials do not appear in it: run failures cross
one redaction boundary before anything is rendered.
Stop, restart, and reset
Before planned maintenance, coordinate with everyone submitting work to this
deployment to stop new submissions. For each RUN_ID returned by a submission,
check:
./infrahub-sync-compose cli runs show RUN_ID
Wait until its execution_state is completed, failed, cancelled,
abandoned, or interrupted. These are terminal execution states; a missing
or other value does not confirm a terminal execution. status can report
READY while work is running: it checks health, not idleness. These checks
cover only known runs; if you cannot account for all submitted work, postpone
maintenance.
Stop and resume the deployment
stop stops all services while preserving their containers and volumes:
./infrahub-sync-compose stop
./infrahub-sync-compose status
Expect STOPPED and exit code 4. To resume the deployment, use start:
./infrahub-sync-compose start
./infrahub-sync-compose status
Expect READY and exit code 0 before submitting work. start starts the
PostgreSQL, object storage, and Prefect dependencies as well as the API and worker.
Restart the API and worker
When the dependencies are running, use restart to restart only the API and
worker processes:
./infrahub-sync-compose restart
./infrahub-sync-compose status
restart waits for READY and preserves durable records. It does not start
stopped dependencies, so use start after a full deployment stop.
Verify a retained configuration
Before maintenance, read a version of an existing registered configuration:
./infrahub-sync-compose cli configs show CONFIG_ID --version VERSION
Replace CONFIG_ID and VERSION with the identifier and version you registered.
Save the output, then repeat the command after the deployment reports READY.
Confirm that config_id, registry_version, package_checksum, and
declared_content match the earlier output. This checks that the configuration
version remains available; READY alone only checks deployment health.
Reset the deployment
reset removes this instance's containers, network, and volumes. Use it only
when you intend to discard the deployment's data:
./infrahub-sync-compose reset <instance identity>
stop, restart, and reset verify the instance labels on the deployment's
resources before changing anything. A resource that is
missing is nothing to remove; a resource that exists under another label is
refused, and the whole operation stops before its first mutation:
infrahub-sync: foreign-resource: these existing resources are not labelled for
instance 3a5dbb1bc6d569b7: container/9c2f1ea41b7d
reset additionally requires you to repeat the identity it just displayed.
There is no forcing flag. It leaves operator.env and secrets/ in place, so
the same credentials serve the next deployment you start from this bundle.
Starting again after a reset is a cold bootstrap, not a resumption. Run init
to generate a new identity — the old one labelled volumes that no longer exist —
and then start. Bootstrap creates the two databases, the product schema, the
bucket, the work pool, and the deployment from nothing, and the deployment
reaches READY with an empty configuration registry, no runs and no artifacts
behind it. Register your package again afterwards; a reset removes what you
registered along with everything else.
Replacing this alpha
This alpha promises no in-place state migration and no backup or restore. Replacing it is the reset above followed by a fresh deployment of the same bundle and the same image:
Use the quickstart lifecycle commands after reset to create the replacement deployment; it receives a new identity and starts empty.
Prior run history, retained plans, and artifacts do not survive that. Keep anything you need outside the deployment before you reset.
When the outcome of a write is uncertain
If a write ends without proving what reached the destination, the deployment does not retry it. Repeating a write whose outcome is unknown is the one thing that could turn an uncertain state into a wrong one, so the run is left terminal and the record carries what is known.
reconciliation_required is a field of the run itself rather than something
buried in its evidence, so deciding whether a run needs reconciling never means
parsing a failure.
cli runs show prints it, beside the phase, the outcome and the Prefect
correlation, and prints it for both of its values: false is an answer, and a
field that appeared only when it was true could not be told apart from one this
command does not report. cli runs results prints what the run itself recorded.
./infrahub-sync-compose cli runs show RUN_ID
./infrahub-sync-compose cli runs results RUN_ID
Two different records mean an uncertain write. A write execution that ended
without reporting records phase interrupted and outcome ambiguous, and sets
reconciliation_required. An apply that began writing and then failed records
phase apply-failed and outcome failed, with
summary.may_have_partially_written set and results.apply_failure naming the
stage, the error type, the operations already applied, and the one that failed.
reconciliation_required is a write-only verdict: an interrupted plan or
verify cannot have written, so it is not set for one, and nothing sets it back
to false.
In both cases, read the record, inspect the destination, and then take a fresh plan run. A new plan reads the destination as it now is, so what it proposes is what is still outstanding. The terminal run is never reopened and no later run inherits its admission.
Developing the bundle
The bundle's own gates run through Invoke, against an image the container gate already built. Neither builds one:
| Command | What it does |
|---|---|
uv run invoke compose.contract | Check the resolved Compose model. Needs the Compose CLI, no daemon. |
uv run invoke compose.lifecycle | Run the Docker-backed lifecycle matrix against the loaded candidate. |
uv run invoke compose.reclaim | Remove the scanner's saved image archives, keeping the OCI layout. |
compose.lifecycle needs uv run invoke image.smoke to have run first: it
addresses the candidate by the configuration digest that build recorded, and
refuses to build one itself, because a rebuild at the same source is a different
digest.