Crucible / Docs

Publish packages and releases

Use one authoring clone per registry and serialize publishers for that clone. The local clone has a release lock, but two machines can still race when they publish the same remote pointers.

#Set up an authoring clone

Create a new signed registry as shown in the quickstart. To attach a smart Git remote at creation time:

apr create acme \
  --remote ssh://git@code.example.com/acme/registry.git \
  --trust-key "$TRUST_KEY" \
  --trust-key-id initial \
  --key "$KEY"
apr push --registry acme --branch stable --set-upstream

To take over an existing registry, configure its public URL and trust anchor with apr add. When invoked as apr, the command materializes an authoring clone as well as consumer state:

apr add https://packages.example.com/acme/ \
  --name acme \
  --trust-key 'acme:Ed25519:BASE64_KEY'

Register locally held signing keys by roster id:

apr keys register initial \
  --key /secure/registry/acme-initial.key \
  --registry acme

An external secret manager can be used without writing its key to APM config:

apr keys register initial \
  --key-command 'secret-tool lookup service aos-registry key initial' \
  --registry acme

The command string is saved in local registry configuration and run through a shell. Keep secrets out of the string itself, and ensure the command writes only an OpenSSH private key to standard output.

#Publish one package

Build the package first, then publish its store path:

STORE_PATH="$(nix build .#pkg-curl --no-link --print-out-paths)"

apr publish "$STORE_PATH" \
  --registry acme \
  --key-id initial

For an ordinary package, apr publish evaluates the target-specific DerivationInventoryV1 and requires the requested path to be that record's exact primary output. Name, version, public metadata, source identity, named outputs, generated documentation, and any package contract all come from the same evaluated record. A contract-bearing package is written with its documentation and complete realization graph in one transaction and one commit; the command never searches for a contract by package name or store path.

The complete ordinary-package transaction always creates one signed commit. --no-commit and manual metadata flags are reserved for --sysroot, whose image catalog entry is outside the ordinary package inventory.

For a grouped change, commit only the intended registry paths with apr commit. It uses the same in-process signer as the other producer commands, requires a trusted key when keys.toml has an active roster, and refuses an already-staged index so unrelated maintainer state cannot leak into the commit. apr sign only signs nonrelease maintenance tags; signed semver releases are immutable:

apr commit packages store registry.toml \
  --registry acme \
  --key-id initial \
  --message "publish the 2026.8 package set"
apr verify --registry acme

Review apr status and apr diff before committing. Name every intended path explicitly; apr commit does not retain a shell-out signing path or silently fall back to a broad git add -A.

Useful package forms include:

# Publish an OS toplevel.
apr publish /nix/store/HASH-aos-system-toplevel \
  --registry acme --sysroot --key-id initial

# Associate prebuilt image artifacts with a sysroot package.
apr publish /nix/store/HASH-aos-system-toplevel \
  --registry acme --sysroot \
  --image-payload /nix/store/HASH-aos-image-qcow2 \
  --image-disk /nix/store/HASH-aos-image-qcow2-disk \
  --image-info /nix/store/HASH-aos-image-qcow2-info \
  --image-format qcow2 --image-uki /nix/store/HASH-uki/aos.efi \
  --image-payload /nix/store/HASH-aos-image-raw \
  --image-disk /nix/store/HASH-aos-image-raw-disk \
  --image-info /nix/store/HASH-aos-image-raw-info \
  --image-format raw --image-uki /nix/store/HASH-uki/aos.efi \
  --key-id initial

Provide one payload, regular-file disk output, regular-file metadata output, format, and exact UKI for each image encoding. The disk and metadata outputs are published through the registry's Nix cache; the payload supplies layout, recovery, and update facts for validation. --previous records the prior package version when maintaining a version chain. --source-drv records source material used by apm source.

#Inspect before release

Review the authoring state and verify that the local store still contains the published closure:

apr status --registry acme
apr diff --registry acme
apr verify --registry acme
apr store verify --registry acme --deep

apr verify checks metadata and closure-graph consistency. apr validate checks that referenced artifacts are reachable from an advertised cache and is most useful after an upload:

apr validate --registry acme --jobs 32

#Run the complete release pipeline

For routine publication, prefer apr release over a hand-assembled sequence:

apr release 2026.8.0 \
  --registry acme \
  --key-id initial \
  --cache-key /secure/registry/acme-narinfo.key \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry

The positional version is the registry snapshot version and has no v prefix. It does not need to match any package version.

When --store-path is supplied, one command publishes the package and the release:

apr release 2026.8.1 \
  --registry acme \
  --store-path "$STORE_PATH" \
  --description "Command-line URL transfer tool" \
  --license curl \
  --maintainer registry@example.com \
  --key-id initial \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry

The package version still defaults to the version parsed from the store-path basename. Use --version when the output name does not carry its package version, or when the package version intentionally differs from the registry snapshot version. Attached image metadata must name that exact package version.

--cache-key is optional. When present, it is a Nix narinfo signing key in name:base64-secret form. It is not the OpenSSH maintainer key used by --key-id.

Preview the ordered plan without mutation:

apr release 2026.8.1 \
  --registry acme \
  --key-id initial \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry \
  --dry-run

Use --resume after an interrupted release. It reuses verified immutable artifacts for the same release; an explicit stage also requires its observed --stage-revision. Do not use it to paper over an unknown or conflicting publisher.

#Upload an unpublished candidate

For a fresh static surface, publish the signed initialization commit before creating the first stage. This establishes its default HEAD and base commit; it does not create a release or assign channel partitions:

apr origin upload --registry acme \
  --upload-url s3://acme-packages/registry

Start from a maintainer branch in the authoring clone. Default and channel branch names are reserved; the stage binds the ordinary source branch and the exact prepared commit. Use the same release porcelain with a stable stage id when the candidate needs inspection before finalization:

git -C "$HOME/.local/share/apm/registries/acme" \
  switch -c dplecki/august-hotfix

apr release 2026.8.1 \
  --registry acme \
  --stage august-hotfix \
  --key-id initial \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry

apr stage list --registry acme
apr --json stage show august-hotfix --registry acme

A new stage starts at revision 1. Record the revision returned by apr stage show; an update with --stage-revision N creates revision N + 1. Resume that exact observed revision after an interrupted upload, retaining the same destination and signing inputs:

apr release 2026.8.1 \
  --registry acme \
  --stage august-hotfix --stage-revision "$REVISION" --resume \
  --key-id initial \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry

A stage records the selected candidate, its prepared signed commit, withheld publication pointers, and immutable inventory. Moving the workspace head does not change a resumed candidate. Inspect the inventory and current revision before finalizing:

apr release 2026.8.1 \
  --registry acme \
  --from-stage august-hotfix --stage-revision "$REVISION" \
  --key-id initial \
  --cache-url https://packages.example.com/acme/ \
  --upload-url s3://acme-packages/registry

Staging and finalization publish a release only. Both --stage and --from-stage reject channel convenience flags. After the first release is published, initialize its channel explicitly and upload the resulting pointers:

AUTHORING_BRANCH="$(git -C "$HOME/.local/share/apm/registries/acme" branch --show-current)"
apr channel init stable 2026.8.1 --registry acme --key-id initial
git -C "$HOME/.local/share/apm/registries/acme" switch stable
apr origin upload --registry acme \
  --upload-url s3://acme-packages/registry
git -C "$HOME/.local/share/apm/registries/acme" switch "$AUTHORING_BRANCH"

apr origin upload deliberately exports the checked-out HEAD. Switch to the registry's default channel (stable here) before uploading its pointers, then restore the maintainer branch. Stage finalization preserves the remote default HEAD; it does not export the live authoring branch as that default.

For an existing channel, use apr channel advance as described in rollouts. The direct release workflow without a stage retains its channel convenience flags.

Default public catalogs and channel partitions keep selecting published releases while a stage is unfinished. A known object digest, authoring ref, cache path, or CDN URL may still reveal uploaded candidate bytes. Use storage access controls when those bytes must be confidential.

Discard a candidate only after checking its revision:

apr stage discard august-hotfix --registry acme \
  --stage-revision "$REVISION"

Discard retains its roots through a minimum 24-hour grace period; shared and released objects keep their other retention roots. apr cache gc honors these roots while collecting eligible aged stage objects and cache pairs; it retains stage records and workspaces. A release stage is independent of a deployment named staging. See Release stages for the state, discovery, transport, and garbage-collection contracts.

#Publish to a smart Git remote

apr push pushes a branch; it does not publish the static cache, channel files, or every signed tag:

apr push --registry acme --branch stable
git -C "$HOME/.local/share/apm/registries/acme" \
  push origin refs/tags/2026.8.0

A smart Git remote is useful for maintainer collaboration and branch or tag consumers. A production channel also needs the generated static origin over HTTP. Maintainer branches hold workspaces; configured channel branch names remain reserved for the published frontier. Use Git as the authoring remote and apr release --upload-url ... for the consumer surface.

#Use focused commands for repair

The release pipeline is composed from commands that remain available for inspection and recovery:

CommandUse
apr tag VERSION --key-id IDCreate a signed release tag
apr sign TAG --key-id IDSign a nonrelease maintenance tag; semver release refs are immutable
apr cache generateRegenerate or upload the static binary cache
apr origin uploadRefresh and upload the static Git origin
apr store backfillRecord closures for older package entries
apr store blessAdd an accepted realization
apr store revokeRevoke an accepted realization

Use these when the intended result is already clear. For a normal release, the guarded apr release ordering is safer and easier to audit.