Repository navigation
Package the native server as an opt-in release-candidate image - #803
Conversation
The opt-in for native's maintenance loop (#797) was missing from the config audit's native-only names, so setting it logged "Unknown DW_ variables set in environment". The native image smoke test showed it. The shipped-environment test now sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server #325's native server has no image yet. This adds one, as a separate repository (durableworkflow/server-native and ghcr.io/durable-workflow/server-native); the PHP image and its Release workflow are unchanged. Dockerfile.native builds the binary with the Shared Server fixtures' pinned compiler (--locked) and copies it onto a digest-pinned Debian 12 slim runtime. It runs as 1000:1000, stops on SIGTERM and listens on 8080. It uses the PHP image's paths, /app/database for SQLite and /app/storage for local external payload objects, whose absolute file:// paths PHP records in the database. It sets DW_RUST_MAINTENANCE_LOOP=1, because there is no separate scheduler container, and never DW_RUST_EXPERIMENTAL: the server still refuses to start without the explicit opt-in. The same image runs the takeover commands. scripts/ci/native-image-smoke.sh checks: - the image config; - the refusal without the opt-in; - a cold start on SQLite, PostgreSQL 17 and MySQL 8.0, with /api/ready, an authenticated /api/cluster/info and a clean SIGTERM exit; - upgrade-inspect refusing a missing SQLite source without creating it. Docker resources are named from RESOURCE_SCOPE, and the workflows remove them in always-steps. Native release candidate (workflow_dispatch from main, for an existing release tag and commit): - each architecture builds on its own runner, pushes by digest only and smoke-tests that pushed digest; - the publish job refuses a tag either registry already has (failing closed on any lookup error); - it tags both smoked digests as one index in both registries and checks that both name exactly those digests; - it attests the index's build provenance in both registries. Native image runs the same build and smoke test on pull requests that change these files, without pushing. The takeover runbook gains concrete commands for the image: stop PHP, inspect, back up and prove the restore by equal fingerprints, take over with that fingerprint, start native, and never restart PHP. It also covers volume ownership and SQLite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release-candidate performance gate measures the artifact operators will run, not a binary mounted into the PHP image. A native-image implementation runs the image named by digest (DW_PORT_NATIVE_IMAGE), with its own entrypoint, user and maintenance loop and native-server's CPU and memory limits. The driver refuses a reference without a digest or one other than the profile's artifacts.native_image, and records it in identity.json; the summarizer counts native-image-server as the runtime. The php and native implementations are unchanged; the binary mount no longer requires DW_PORT_NATIVE_BINARY when native does not run. Checked: Compose renders the php, native and native-image profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security's review of the native image draft (server#325 comment 6431969727): the start command wrote DW_AUTH_TOKEN=<token> on the command line. It now passes the exported value by name, as DB_PASSWORD already was, so no credential enters arguments or shell history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security's advisory on b2d62ea: native-image mode compared the image only when the profile named one, so a profile without artifacts.native_image accepted any digest-pinned image. The driver now refuses such a profile, so every native-image result binds to the profile's exact image. Checked: a profile without the field and one naming another image are refused before anything starts; the matching profile proceeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SonarCloud flagged two new security findings on this PR, and reading the first exposed a real defect: - b2d62ea inserted native-image-server in the middle of native-server's block, so native-server lost its depends_on and curl healthcheck to the new service, which has no curl. native-server is back to main's block. native-image-server reuses its environment through a YAML anchor (no second copy of the disposable benchmark password, yaml:S2068), waits for MySQL, and checks readiness with a plain GET over bash's /dev/tcp. - The smoke probe built an http:// URL string (shell:S5332). It now uses http.client with the host and port: the server answers plain HTTP on the job's private network. The probe's positional parameters are named locals. Checked: Compose renders both services with their own depends_on and healthcheck; the /dev/tcp check answers HTTP/1.0 200 OK against the image; the local smoke test passes on SQLite, PostgreSQL and MySQL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security advisory re-check of exact head 050beac: The corrected Compose boundary is sound: native-server has its original MySQL dependency and curl readiness check again; native-image-server separately inherits only the intended disposable benchmark environment, waits for MySQL, and probes readiness without adding curl. The http.client smoke probe keeps the host and path as data, and the PR workflow has read-only contents permission with no publication credentials. One actionable migration-safety correction remains in docs/rust-server-upgrade-takeover.md lines 68–92. Every inspection, fingerprint, irreversible takeover, and final start command uses durableworkflow/server-native:. The release workflow records a verified multi-architecture index digest, but registry tags remain mutable outside that workflow. If the tag moves between steps, the operator can inspect with one executable and mutate/start with another. Please export one exact image reference from the release receipt, for example IMAGE=durableworkflow/server-native@sha256:, and use This is a focused correction to the unreleased runbook, not a standing review gate. I found no further security correction in the Sonar follow-up itself. |
|
Additional exact-head release-path finding from read-only registry inspection: Dockerfile.native pins the Rust builder as mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0. The registry identifies that digest as an OCI image manifest for linux/amd64, not a multi-architecture index. Its annotation also identifies it as the amd64 form of rust:1-slim-bookworm. The pinned Debian runtime digest is correctly a multi-architecture OCI index and includes both linux/amd64 and linux/arm64/v8. The release workflow, however, runs a native linux/arm64 build on ubuntu-24.04-arm without setting up emulation. Its builder FROM therefore cannot yield the intended arm64 binary from the current amd64-only builder digest; the existing PR workflow proves only amd64. Please pin a reviewed Rust builder index digest that contains both amd64 and arm64, or select reviewed per-architecture builder digests explicitly while preserving the same compiler/toolchain contract. Then exercise the arm64 Dockerfile build before treating the two-architecture release path as qualified. This is a concrete multi-architecture release-path correction, not a standing security gate or a reason to block unrelated Rust source work. |
The checkpoint names the verified 2.11.4 artifacts, the Core 2.14 expectation changes native now follows, the poll_transport position and the Workflow #931 boundary, with what was checked. The Rust port's current state lists #799 and #801 as merged, #803 and this move as next, and 2.11.4 as the compared reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
To reduce correction turnaround, read-only registry resolution of |
Security found that the pinned Rust builder (ba81bc3e…) is a single linux/amd64 manifest, so the release's ubuntu-24.04-arm job could not build natively (server#803 comment 6433955707). The smoke test's MySQL pin (62fb722c…) is amd64-only as well. - Dockerfile.native builds from Security's verified Rust 1.99.0/bookworm multi-architecture index 114c7a44… (amd64 b36c2467…, arm64 e393e07b…, comment 6434047541). - The smoke test runs MySQL 8.0 from its index 7dcddc01…, whose amd64 entry is the 62fb722c… image already pinned and whose arm64 entry is 213bbfaf…. The Python probe, PostgreSQL and the Debian runtime were already multi-architecture indexes. - Native image runs on pull requests as a matrix: linux/amd64 on ubuntu-latest and linux/arm64 on ubuntu-24.04-arm, each building and smoke-testing natively, with a receipt per architecture. Checked: a local linux/amd64 build from the new builder passes the smoke test on SQLite, PostgreSQL and MySQL; both organization policies pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security's re-check of 050beac (server#803 comment 6433835833): every inspection, fingerprint, the irreversible takeover and the final start named durableworkflow/server-native:<tag>. A tag can move between steps, so the operator could inspect with one executable and take over or start with another. The runbook now exports one reference, the multi-architecture index digest the release receipt records (IMAGE=durableworkflow/server-native@sha256:...), pulls it, keeps it with the backup and uses "$IMAGE" in every command through startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security delta review at exact head |
|
|
Security terminal verification for exact head |
|
Native build and smoke at exact head
Each image was built natively on its own architecture from the multi-architecture Rust index
Each receipt is the run's |
|
General release-path review accepted at exact head Reviewed the dispatch-only/main-ref publication boundary, trusted release tooling versus immutable tagged source, per-architecture digest outputs and smoke-before-tag dependency, exact two-platform index verification in both registries, non-overwriting tag checks, provenance steps, nonroot image configuration, explicit experimental opt-in, and backup/takeover commands retaining one immutable image reference. The matched-performance extension records and requires its exact image digest; its native image uses its own entrypoint and maintenance loop. The exact-head native CI run 40454456424 passes both native amd64 and arm64 build/smoke/cleanup jobs. Required feature, fixture-result and genuine Sonar checks are green. I did not build Rust locally or run a takeover. This accepts the source and release path; it does not qualify a published native release, the broader cutover, or production deployment. Merge itself publishes no image. |
|
Merged through the merge queue at 2026-10-27T06:31:57Z, 22 minutes after enqueue (06:10:01Z, pinned to head
Nothing is published yet. The native release ( |



Part of #325. Design: #325 comments 6431039138 and 6431101579. Review request: 6431904291. Security design acceptance: 6431969727.
The native server gets an opt-in release-candidate image in its own repositories,
durableworkflow/server-nativeandghcr.io/durable-workflow/server-native. The PHP image, its Release workflow and its defaults are unchanged.1. The image (
ed7a175a6)Dockerfile.native:rust@sha256:114c7a44…: amd64b36c2467…, arm64e393e07b…), runscargo build --release --lockednatively on each architecture. The build context is therust/crate only.debian@sha256:7ff6ba31…slim, holding only the binary. TLS uses rustls with compiled-in roots, and SQLite is bundled.1000:1000, stops onSIGTERM, exposes8080.WORKDIR /app, SQLite at/app/database/database.sqlite, volumes/app/databaseand/app/storage. PHP records local external payload objects by absolutefile:///app/storage/...path, and native's default local root is<cwd>/storage/app/external-payloads/<ns>. A taken-over installation therefore keeps its volumes at the same paths.DW_RUST_MAINTENANCE_LOOP=1, since there is no separate scheduler container.DW_RUST_EXPERIMENTALis never set, so the server still refuses to start without the explicit opt-in.scripts/ci/native-image-smoke.sh <image> <platform> <receipt>checks:/api/ready, an authenticated/api/cluster/infoand aSIGTERMexit code 0;upgrade-inspectexits non-zero withupgrade_inspection_unavailableon a missing SQLite source, without creating it.Docker resources are named from
RESOURCE_SCOPE, and the workflows remove them in always-steps.Native release candidate (
native-release.yml,workflow_dispatchfrom main, inputstagandrelease_commit):One job per architecture (
ubuntu-latest,ubuntu-24.04-arm). Each job:github.shaand the immutable tag;provenance: false);A failed smoke leaves no tag.
A publish job (
scripts/ci/native-release-publish.sh):Native image (
native-image.yml): pull requests that change these files build the image and run the same smoke test natively on linux/amd64 (ubuntu-latest) and linux/arm64 (ubuntu-24.04-arm). It never pushes. The smoke test's MySQL is the 8.0 multi-architecture index7dcddc01…; its amd64 entry is the62fb722c…image the fixtures pin.2. The takeover runbook (
docs/rust-server-upgrade-takeover.md)A new section gives concrete commands for the image:
Every command uses one exact image,
IMAGE=durableworkflow/server-native@sha256:<index digest from the release receipt>, never a tag that could move between steps (56eb1d34b). Credentials pass by name (-e DB_PASSWORD,-e DW_AUTH_TOKEN), following Security's review (7e28e5952). The section also covers:www-data, native runs as 1000);3. The matched performance harness runs the published image (
b2d62ea70,d638cc156)The release-candidate performance gate measures the artifact operators run.
run-server-port-matched.sh … native-imageruns the image named by digest (DW_PORT_NATIVE_IMAGE), with its own entrypoint, user and maintenance loop, undernative-server's limits.artifacts.native_image, and the driver refuses any other image. This follows Security's advisory.phpandnativeimplementations are unchanged.Review fixes
050beac16: SonarCloud's findings exposed thatnative-image-serverhad been inserted in the middle ofnative-server's Compose block.native-serveris restored to main's block.native-image-servershares its environment through a YAML anchor and checks readiness over bash's/dev/tcp. The smoke probe useshttp.client.be4fcbc11: Security found the builder and MySQL pins were amd64-only. Both are now multi-architecture indexes, and the PR matrix builds and smokes natively on arm64.56eb1d34b: the runbook is bound to one exact image digest.Checks run
sha256:53837092…, from a local build of this source. It passes on all three databases. Startup logs no warnings, afterDW_RUST_MAINTENANCE_LOOPwas registered in Release message streams at retention, and keep PHP streams at takeover #801.qualification_policy.py --target server.Before first publication
dockerhubenvironment's ability to push to the new repositories, and whether they must exist before a first run.Nothing here pushes to a registry.
🤖 Generated with Claude Code