Container publication flow
Overview
Manual Enterprise publication builds controller and runtime OCI archives for
Linux amd64 and arm64, checks both variants, and transfers the tested bytes to
public GHCR packages. Each package receives one multi-platform index digest.
With publish_chart: true, a separate protected job tags those digests with the
OCE version and publishes a Helm chart. Image-only publication is the default.
Entry Points
.github/workflows/container-publish.yml:jobs.validate: manual dispatch onmainwith its exact source SHA, successful main-push CI run ID, publish flag, optional mutable image tag, and opt-in chart flag.scripts/ci/container-release.mjs:main: validation, smoke, seal, and publication commands called by the workflow.scripts/ci/chart-release.ts:publish: version-tag and chart publication after the image receipt exists.- Publication requires pre-existing public packages. The manual dispatch authorizes publication without a separate environment approval. GitHub grants manual dispatch to repository writers, including maintainers.
Flow
graph TD
A["Operator selects main SHA and successful CI"] --> B["Validate source, CI and approved base"]
B --> C["Pin timestamps to the source commit"]
C --> D["Build controller and runtime on native AMD64 and ARM64"]
D -->|controller| E["Verify native image and run smoke"]
D -->|runtime| R["Retain untested OCI build"]
R --> S["Fresh native runner verifies build and runs smoke"]
S --> H
E --> H["Canonicalize descriptors and assemble OCI indexes"]
H -->|either fails| X["Stop before publication"]
H -->|both pass| F["Seal and upload each archive"]
F -->|publish false| G["Finish with retained artifacts"]
F -->|publish true| I["Recheck source, CI, seals and public packages"]
I --> J["Copy and verify both immutable source tags"]
J --> K["Copy and verify selected mutable aliases"]
K --> L["Recheck digests and write image receipt"]
L -->|publish_chart false| O["Finish image publication"]
L -->|publish_chart true| M["Separate job tags image digests and pushes chart"]
M --> N["Pull chart, verify bytes and write release receipt"]Source review does not prove a hosted build or registry transfer succeeded.
Execution Trace
1. Admit the source and initialize preparation
scripts/ci/container-release.mjs:validate verifies the trusted workflow, exact
main source and successful CI identity, approved Node base digest, and the optional
image tag. An empty tag selects latest; invalid or reserved tags fail before
preparation. No-push preparation accepts private or public source only when
PUBLISH is the exact string "false". Publication and recovery require the
public Enterprise repository and public packages. Publication also checks the
main-only environment branch policy. No-push preparation has no package write
permission or protected-environment credentials.
.github/workflows/container-publish.yml:jobs.prepare calls the reusable
.github/workflows/container-check.yml:jobs.prepare image/architecture matrix. Controller and runtime each build on native AMD64 and ARM64 Linux runners.
See shared CI settings for native runner defaults and overrides.
Runtime source verification
To verify runtime changes locally, first resolve the build arguments, then run:
docker build -f deploy/runtime/Dockerfile \ "${openclaw_build_args[@]}" --target openclaw-bridge-tests .Runtime jobs run bridge suites, CUA tests and source typechecks in openclaw-bridge-tests.
Failure blocks assembly, smoke and upload. Assembly uses the same checkout, build arguments,
base and platform. Local assembly can run independently of
local source verification.
The resources step checks architecture, CPU, memory and cgroup-v2 ancestor
limits, then logs capacity and Node compiler jobs. ARM runtime can select eight;
unknown or smaller limits retain four. Both build stages consume that output.
See build settings for the budget.
Before runtime smoke, scripts/ci/setup-tools.sh installs checksum-pinned kubectl, k3d,
Helm and yq on either architecture. The standard AMD64 override requires 36 GiB free.
Below that threshold, cleanup checks required roots and skips unsafe paths.
Larger runners need no SDK cleanup.
Buildx runs at most two steps concurrently. Default Blacksmith runners cache BuildKit
layers on local sticky disks scoped by image and architecture. Other runners use the
GitHub Actions cache with the same scope. Maintainers can dispatch container-check.yml
directly or through manual CI to verify a branch without package-write permission
or publication. Standalone checks use the source's CI base. Manual CI and publication
use the approved repository base, which publication verifies against the source.
Builds export OCI directories without registry credentials. BuildKit normalizes image
and filesystem timestamps to SOURCE_DATE_EPOCH from the exact OCE commit, including
on cold-cache rebuilds.
scripts/lib/openclaw-source.mjs:readOpenclawSource resolves the runtime's commit,
archive checksum, and build timestamp from deploy/runtime/openclaw-source.json.
The Dockerfile rejects missing, malformed, or conflicting build arguments before
downloading source. Manifest changes invalidate build caches; credential-service
targets remain independent.
deploy/runtime/Dockerfile:openclaw-source verifies the OpenClaw source archive
and applies the checksum-verified patches described in the
runtime recipe. Both installs use
frozen lockfiles and upstream's selected-plugin manifests, retaining required
bundled plugins plus Codex and Slack. The standalone Codex command links to the
plugin's installation. Build stages use full Bookworm; final images use separately
pinned slim bases. The controller requires Node 24.22.1+ within the 24 series.
The runtime requires matching Node 24.22.1 Bookworm bases. Its node-source stage checks
the archive and patch digests in deploy/runtime/node/source.json before applying the
worker-termination repair. node-compiled builds native Node with full ICU and runs
the worker regression and adjacent loader tests. Slim Bookworm repeats the tests
with the copied executable. Missing libraries, ABI changes or regression failures
stop assembly. The repository-credential service retains its original base.
Both bases apply updates from mutable Debian repositories, even with pinned image digests.
Every index refresh uses
apt-get update --error-on=any to stop builds on incomplete downloads.
deploy/runtime/node/build.sh records the source, patch, ABI and executable digest.
scripts/build-runtime-assets.mjs:inventory checks its running executable's digest
before recording node in final runtime provenance. Image startup compares that
record with the checkout and installed executable. Native qualification requires
packaged Gateway restart tests.
deploy/runtime/Dockerfile and deploy/runtime/repository-credentials/Dockerfile.client
retry pinned GitHub CLI archive and manifest downloads up to six times, including partial
transfers and permanent errors. Curl resets partial files. Before extraction or execution,
both recipes check the pinned manifest digest, then its archive checksum.
Exhausted retries or a checksum failure stop the build immediately.
Publication builds use the default package registries. Operators building from source can configure a package registry mirror for both the pinned pnpm download and dependency installation.
deploy/runtime/Dockerfile:curl-packages builds the Bookworm curl backport
from checksum-pinned Debian source and runs its proxy authentication and TLS
tests without external networking. deploy/runtime/curl/build.sh inserts the
CVE-2026-86414 fix before Debian's backend patches, so both OpenSSL curl and
Git's GnuTLS library reject origin authentication challenges on CONNECT. The
final runtime installs all three generated Debian packages together and records
their source and package checksums under /usr/share/doc/curl/oce/. Debian
package metadata and the changelog retain the base version and the +oce1
backport revision; a vulnerability scanner may still report the upstream CVE.
The final install compares fresh APT indexes with the recorded Debian base
version before selecting local packages, so a cached backport cannot silently
replace a newer Debian security update.
The build runs the caller-authority, Gateway recovery and Linux private-store tests offline. The private-store bridge normalizes inherited setgid only for a new owner-only directory, so native first use can create private SQLite stores under the Sandbox workspace. Existing parent permissions and filesystem admission checks remain unchanged. The final image records the patch checksums in its provenance.
The Codex profile patch
requires the requested named profile before returning a thread. Non-full-access
refusals report fixed request-source, selector-agreement and response-comparison
categories, omitting profile names and configuration. Missing metadata or a
different returned ID fails. The message asks administrators to check profile
availability and runtime compatibility before retrying. Full-access refusals
classify sandbox or response-shape mismatches; acceptance still requires
sandbox.type equal to dangerFullAccess.
The runtime disables npm background update checks. See the runtime defaults.
scripts/build-runtime-assets.mjs prunes development/QA files and unreachable
pnpm entries, retaining runtime templates, skills, help, and importer-specific
dependencies. It writes provenance with the manifest's commit and archive
checksum plus patch, lockfile, and inventory hashes. The runtime copies this
tree without gzip or a second Codex installation. After permission normalization,
inventory mode updates contents.json, runtimeContentsSha256, and Codex
package/binary identity. Upstream import-closure and native-addon checks remain;
see the runtime recipe.
org.openclaw.image.revision must match packaged provenance;
org.opencontainers.image.revision and receipt sourceSha identify OCE.
Final image checks require @openclaw/proc-safe/reaper to return true from
isSupported().
2. Verify and assemble both platform variants
Fresh native runners execute .github/workflows/container-check.yml:jobs.runtime-smoke.
container-release.mjs:runtimeBuild binds build.json (native: false) to source,
workflow, run/attempt, CI, base, platform and digest. Receivers verify that identity
and referenced OCI blobs, rejecting preexisting platform.json.
built-runtime-* cannot qualify for publication. Controllers retain one-job build/smoke.
scripts/ci/container-release.mjs:smoke requires the selected native architecture.
It binds the OCI export to the build digest, loads it with Skopeo, checks the
loaded config and architecture, and runs startup tests by immutable image ID
with native deadlines. For containerd, scripts/upgrade-image-identity.py exports
and hash-verifies the manifest/config against the original OCI config, binding
configuration and ordered layer diff IDs. The legacy store compares config IDs
directly. Successful smoke writes platform.json, binding source, workflow,
run/attempt, CI, image, base and platform digest. Failed smoke cannot upload
platform-* artifacts; recovery requires both runtime smoke jobs to succeed.
.github/workflows/container-publish.yml:jobs.assemble downloads both qualified
platform artifacts from this run/attempt. container-release.mjs:assemble
rejects receipt, platform, size or hash mismatches before linking blobs into one
OCI layout. Descriptors retain media type, digest, size and platform, excluding
exporter annotations such as timestamps and local names. Assembly writes the
index and archive without rebuilding, then removes temporary layouts.
readArchivePlatforms verifies exactly one AMD64 and one ARM64 Linux manifest
with matching configs and digests. Assembly needs no registry access.
scripts/ci/prepare.mjs:prepareRuntimeImageSmoke imports the verified image into
disposable k3d. It reuses the Images and Packaging lane's Codex seccomp derivation
and sandbox probes, passing profile and ownership state to Docker tests. Smoke
cleans up on exit. After command interruption, the workflow attempts cleanup if
the runner remains available; runner loss can prevent cleanup.
3. Seal and enter publication
scripts/ci/container-release.mjs:seal rechecks the platform contents and records
the archive hash, index digest, each platform’s manifest and config digests,
source, workflow run and attempt, CI identity, and approved base. Both prepared artifacts must exist
before .github/workflows/container-publish.yml:jobs.publish can start. The
publish job runs only when the operator selected publish: true.
No-push runs end with artifacts. See operator instructions for retention and package access.
4. Publish and hand off immutable references
scripts/ci/container-release.mjs:publishPrepared checks both seals, archive hashes,
index digests, and destinations before copying either image. It repeats source,
CI, environment branch policy, and package checks during transfer. A matching existing source tag is retained;
a conflicting tag or ambiguous registry error fails. An absent tag receives the
entire index and both child manifests through Skopeo --all --preserve-digests.
After both source tags verify, ordinary publication copies each sealed archive to
latest or the selected custom alias, then checks both source and alias digests.
The custom alias replaces latest for that dispatch; recovery does not move an
alias. The two aliases are updated separately, so a failed run can leave them on
different digests. The receipt is written only after both verify.
Package metadata must report the expected name and public visibility. Reported repository linkage must match the public Enterprise repository. Omitted linkage is accepted without review-history lookups; package setup owns that connection.
Each remote index digest must match before the receipt is written. Deployment uses that index digest, letting the container runtime select its architecture. A partial failure leaves existing published bytes intact. Recovery consumes the same retained multi-platform archives and original producer identity; it does not rebuild them. Old amd64-only seals cannot satisfy this platform contract.
The optional jobs.publish-chart downloads this run/attempt's image receipt only
after jobs.publish succeeds. scripts/ci/chart-release.ts requires
both entries to match the current source, producer run, CI run, package names,
and immutable digests. Root package.json, the chart version, and appVersion
must agree before a version tag is written. The staged chart carries the source
SHA and both digest references in annotations; its default controller image is
the verified controller digest. Operator values can override that default.
Both jobs share the protected environment. Workflow-level concurrency retains
the publication lock across preparation and both writes; jobs never reacquire it.
The chart publisher checks image version tags and any existing chart before writing. Existing tags must resolve to the receipt's digests;
an existing chart version must have identical packaged files. It rechecks the trusted
source, CI, environment, and public chart package before each write. After
helm push, it pulls the chart, compares its packaged files and new-push archive bytes, inspects the remote
manifest digest, and writes a separate chart-publication.json. A partial
failure requires a new full dispatch with identical source; chart-only reruns
lack the new attempt's image receipt. Chart failure leaves image success intact
but fails the combined run. jobs.summary reports both outcomes.
The receipt binds the chart manifest digest observed after the pull. This
assumes the protected publication workflows and trusted package administrators
are the only package writers; their workflow concurrency group does not exclude
independent GHCR writers. A tag change between the pull and digest lookup could
make the receipt refer to a different manifest. The operator's
single-writer requirement
owns package-write access and coordination.
Debugging and Verification
- For rejected source arguments, resolve the manifest values and rebuild without bypassing checksum verification.
- Run
node --test tests/integration/container-{release,resume,promote}.test.mjsfor platform/archive validation and release gate coverage. The archive case uses real OCI blobs and tar; recovery uses transport fixtures. - Run the registry integration to check real Skopeo alias replacement against a disposable local registry. This does not prove GHCR access.
- Run
actionlint .github/workflows/container-publish.ymlfor workflow syntax. - In a hosted preparation, require smoke output for both architectures of both images. A config mismatch, missing platform, or failed startup blocks upload.
- Repeat publication for the same source and CI evidence to prove that the registry index identity is stable; the second run must retain the existing source tag. Mutable external package inputs can still produce a legitimate conflict, which remains fail-closed.
- Use
docker buildx imagetools inspecton both the source tag and selected alias. Require Linux amd64 and arm64 plus the receipt's index digest. Startup checks do not establish native ARM64 performance, cluster behavior, or model turns.
