Skip to content

Commit cd016d2

Browse files
authored
Merge pull request #803 from durable-workflow/rust-325-native-image
Package the native server as an opt-in release-candidate image
2 parents b5f3146 + 56eb1d3 commit cd016d2

11 files changed

Lines changed: 775 additions & 20 deletions

‎.github/workflows/native-image.yml‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Native image
2+
3+
# Builds the native (Rust) server image from Dockerfile.native and runs the
4+
# same smoke test the native release runs (Server #325). Only changes to the
5+
# image's own files run it: the release workflow is dispatch-only, so this is
6+
# where a pull request finds out that the Dockerfile, its ignore file or the
7+
# smoke and publish scripts no longer work. Nothing is pushed.
8+
9+
on:
10+
pull_request:
11+
paths:
12+
- Dockerfile.native
13+
- Dockerfile.native.dockerignore
14+
- scripts/ci/native-image-smoke.sh
15+
- scripts/ci/native-release-publish.sh
16+
- .github/workflows/native-image.yml
17+
- .github/workflows/native-release.yml
18+
19+
permissions:
20+
contents: read
21+
22+
concurrency:
23+
group: native-image-${{ github.event.pull_request.number }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
smoke:
28+
name: Build and smoke the native image (${{ matrix.platform }})
29+
runs-on: ${{ matrix.runner }}
30+
timeout-minutes: 60
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
include:
35+
- platform: linux/amd64
36+
arch: amd64
37+
runner: ubuntu-latest
38+
- platform: linux/arm64
39+
arch: arm64
40+
runner: ubuntu-24.04-arm
41+
env:
42+
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
43+
steps:
44+
- name: Check out
45+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
46+
47+
- name: Set up Docker Buildx
48+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
49+
50+
- name: Build the image into the local daemon
51+
timeout-minutes: 45
52+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
53+
with:
54+
context: .
55+
file: Dockerfile.native
56+
platforms: ${{ matrix.platform }}
57+
pull: true
58+
provenance: false
59+
load: true
60+
push: false
61+
tags: ${{ env.RESOURCE_SCOPE }}:smoke
62+
63+
- name: Smoke-test the image
64+
env:
65+
PLATFORM: ${{ matrix.platform }}
66+
ARCH: ${{ matrix.arch }}
67+
run: bash scripts/ci/native-image-smoke.sh "$RESOURCE_SCOPE:smoke" "$PLATFORM" "native-image/smoke-$ARCH.json"
68+
69+
- name: Remove this job's smoke containers and network
70+
if: always()
71+
run: |
72+
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
73+
docker network rm "$RESOURCE_SCOPE" || true
74+
75+
- name: Keep the smoke receipt
76+
if: always()
77+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
78+
with:
79+
name: native-image-smoke-${{ matrix.arch }}
80+
path: native-image
81+
retention-days: 7
82+
if-no-files-found: ignore
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
name: Native release candidate
2+
3+
# Publishes the native (Rust) server image for an existing immutable release
4+
# tag (Server #325). Each architecture builds on its own runner and pushes by
5+
# digest only; that exact digest is pulled back and smoke-tested there, so a
6+
# failed smoke leaves no tag. The publish job then refuses tags that already
7+
# exist, gives both smoked digests
8+
# the release tag in both registries as one index, verifies it, and attests
9+
# its build provenance. The PHP image and its Release workflow are unchanged.
10+
11+
run-name: Native ${{ inputs.tag }} at ${{ inputs.release_commit }}
12+
13+
on:
14+
workflow_dispatch:
15+
inputs:
16+
tag:
17+
description: 'Existing immutable release tag to publish natively (for example 2.12.0-rc.1)'
18+
required: true
19+
release_commit:
20+
description: 'Full source commit of that tag'
21+
required: true
22+
23+
permissions:
24+
contents: read
25+
26+
concurrency:
27+
group: native-release-${{ inputs.tag }}
28+
cancel-in-progress: false
29+
30+
env:
31+
DOCKERHUB_IMAGE: durableworkflow/server-native
32+
GHCR_IMAGE: ghcr.io/durable-workflow/server-native
33+
34+
jobs:
35+
build-amd64:
36+
name: Build and smoke linux/amd64
37+
if: github.ref == 'refs/heads/main'
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 75
40+
environment: dockerhub
41+
permissions:
42+
contents: read
43+
packages: write
44+
env:
45+
PLATFORM: linux/amd64
46+
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
47+
outputs:
48+
digest: ${{ steps.build.outputs.digest }}
49+
steps:
50+
- name: Check out trusted release tooling
51+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
52+
with:
53+
ref: ${{ github.sha }}
54+
55+
- name: Check out immutable release source
56+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
57+
with:
58+
path: release-source
59+
ref: refs/tags/${{ inputs.tag }}
60+
61+
- name: Verify the release tag names the planned commit
62+
env:
63+
GH_TOKEN: ${{ github.token }}
64+
RELEASE_TAG: ${{ inputs.tag }}
65+
RELEASE_COMMIT: ${{ inputs.release_commit }}
66+
run: |
67+
set -euo pipefail
68+
scripts/ci/verify-release-tag-source.sh
69+
test "$(git -C release-source rev-parse HEAD)" = "$RELEASE_COMMIT"
70+
71+
- name: Set up Docker Buildx
72+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
73+
74+
- name: Log in to Docker Hub
75+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
76+
with:
77+
username: ${{ secrets.DOCKERHUB_USERNAME }}
78+
password: ${{ secrets.DOCKERHUB_TOKEN }}
79+
80+
- name: Log in to GHCR
81+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
82+
with:
83+
registry: ghcr.io
84+
username: ${{ github.actor }}
85+
password: ${{ secrets.GITHUB_TOKEN }}
86+
87+
- name: Build and push linux/amd64 by digest only
88+
id: build
89+
timeout-minutes: 60
90+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
91+
with:
92+
context: release-source
93+
file: release-source/Dockerfile.native
94+
platforms: linux/amd64
95+
pull: true
96+
provenance: false
97+
labels: |
98+
org.opencontainers.image.revision=${{ inputs.release_commit }}
99+
dev.durable-workflow.release.tag=${{ inputs.tag }}
100+
dev.durable-workflow.release.run-id=${{ github.run_id }}
101+
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
102+
103+
- name: Smoke-test the pushed linux/amd64 digest on its own architecture
104+
env:
105+
IMAGE: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
106+
run: bash scripts/ci/native-image-smoke.sh "$IMAGE" "$PLATFORM" native-release/smoke-amd64.json
107+
108+
- name: Remove this job's smoke containers and network
109+
if: always()
110+
run: |
111+
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
112+
docker network rm "$RESOURCE_SCOPE" || true
113+
114+
- name: Keep the linux/amd64 smoke receipt
115+
if: always()
116+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
117+
with:
118+
name: native-smoke-amd64
119+
path: native-release
120+
retention-days: 30
121+
if-no-files-found: ignore
122+
123+
build-arm64:
124+
name: Build and smoke linux/arm64
125+
if: github.ref == 'refs/heads/main'
126+
runs-on: ubuntu-24.04-arm
127+
timeout-minutes: 75
128+
environment: dockerhub
129+
permissions:
130+
contents: read
131+
packages: write
132+
env:
133+
PLATFORM: linux/arm64
134+
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
135+
outputs:
136+
digest: ${{ steps.build.outputs.digest }}
137+
steps:
138+
- name: Check out trusted release tooling
139+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
140+
with:
141+
ref: ${{ github.sha }}
142+
143+
- name: Check out immutable release source
144+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
145+
with:
146+
path: release-source
147+
ref: refs/tags/${{ inputs.tag }}
148+
149+
- name: Verify the release tag names the planned commit
150+
env:
151+
GH_TOKEN: ${{ github.token }}
152+
RELEASE_TAG: ${{ inputs.tag }}
153+
RELEASE_COMMIT: ${{ inputs.release_commit }}
154+
run: |
155+
set -euo pipefail
156+
scripts/ci/verify-release-tag-source.sh
157+
test "$(git -C release-source rev-parse HEAD)" = "$RELEASE_COMMIT"
158+
159+
- name: Set up Docker Buildx
160+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
161+
162+
- name: Log in to Docker Hub
163+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
164+
with:
165+
username: ${{ secrets.DOCKERHUB_USERNAME }}
166+
password: ${{ secrets.DOCKERHUB_TOKEN }}
167+
168+
- name: Log in to GHCR
169+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
170+
with:
171+
registry: ghcr.io
172+
username: ${{ github.actor }}
173+
password: ${{ secrets.GITHUB_TOKEN }}
174+
175+
- name: Build and push linux/arm64 by digest only
176+
id: build
177+
timeout-minutes: 60
178+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
179+
with:
180+
context: release-source
181+
file: release-source/Dockerfile.native
182+
platforms: linux/arm64
183+
pull: true
184+
provenance: false
185+
labels: |
186+
org.opencontainers.image.revision=${{ inputs.release_commit }}
187+
dev.durable-workflow.release.tag=${{ inputs.tag }}
188+
dev.durable-workflow.release.run-id=${{ github.run_id }}
189+
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
190+
191+
- name: Smoke-test the pushed linux/arm64 digest on its own architecture
192+
env:
193+
IMAGE: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
194+
run: bash scripts/ci/native-image-smoke.sh "$IMAGE" "$PLATFORM" native-release/smoke-arm64.json
195+
196+
- name: Remove this job's smoke containers and network
197+
if: always()
198+
run: |
199+
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
200+
docker network rm "$RESOURCE_SCOPE" || true
201+
202+
- name: Keep the linux/arm64 smoke receipt
203+
if: always()
204+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
205+
with:
206+
name: native-smoke-arm64
207+
path: native-release
208+
retention-days: 30
209+
if-no-files-found: ignore
210+
211+
publish:
212+
name: Tag, verify and attest the native index
213+
needs: [build-amd64, build-arm64]
214+
if: github.ref == 'refs/heads/main'
215+
runs-on: ubuntu-latest
216+
timeout-minutes: 20
217+
environment: dockerhub
218+
permissions:
219+
contents: read
220+
packages: write
221+
id-token: write
222+
attestations: write
223+
steps:
224+
- name: Check out trusted release tooling
225+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
226+
with:
227+
ref: ${{ github.sha }}
228+
229+
- name: Set up Docker Buildx
230+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
231+
232+
- name: Log in to Docker Hub
233+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
234+
with:
235+
username: ${{ secrets.DOCKERHUB_USERNAME }}
236+
password: ${{ secrets.DOCKERHUB_TOKEN }}
237+
238+
- name: Log in to GHCR
239+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
240+
with:
241+
registry: ghcr.io
242+
username: ${{ github.actor }}
243+
password: ${{ secrets.GITHUB_TOKEN }}
244+
245+
- name: Tag and verify the index in both registries
246+
id: index
247+
env:
248+
RELEASE_TAG: ${{ inputs.tag }}
249+
DIGEST_AMD64: ${{ needs.build-amd64.outputs.digest }}
250+
DIGEST_ARM64: ${{ needs.build-arm64.outputs.digest }}
251+
run: bash scripts/ci/native-release-publish.sh
252+
253+
- name: Attest the index's build provenance in GHCR
254+
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
255+
with:
256+
subject-name: ${{ env.GHCR_IMAGE }}
257+
subject-digest: ${{ steps.index.outputs.digest }}
258+
push-to-registry: true
259+
260+
- name: Attest the index's build provenance in Docker Hub
261+
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
262+
with:
263+
subject-name: index.docker.io/${{ env.DOCKERHUB_IMAGE }}
264+
subject-digest: ${{ steps.index.outputs.digest }}
265+
push-to-registry: true

‎Dockerfile.native‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Durable Workflow native (Rust) server, published as a release candidate
2+
# (Server #325). It is opt-in: the server refuses to start without
3+
# DW_RUST_EXPERIMENTAL=1. The published PHP image remains the default.
4+
#
5+
# The same image runs the takeover commands an operator uses to move a
6+
# stopped, backed-up PHP database to native: upgrade-inspect,
7+
# upgrade-queue-inspect, upgrade-payload-inspect, upgrade-fingerprint,
8+
# upgrade-prepare-sqlite and upgrade-takeover
9+
# (docs/rust-server-upgrade-takeover.md).
10+
11+
# Rust 1.99.0 on Debian 12 (glibc 2.36, matching the runtime below), pinned
12+
# as its multi-architecture index so each architecture builds natively:
13+
# linux/amd64 b36c246742b4…, linux/arm64 e393e07b3c3c… (Security verified
14+
# the index, server#803 comment 6434047541).
15+
FROM mirror.gcr.io/library/rust@sha256:114c7a4425406451c2866b6aafe69fe29b1b298832db1277d411ac73c82d04d6 AS build
16+
ARG CARGO_BUILD_JOBS=2
17+
ENV CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS} \
18+
CARGO_INCREMENTAL=0 \
19+
CARGO_PROFILE_RELEASE_DEBUG=0
20+
WORKDIR /source/rust
21+
COPY rust/ /source/rust/
22+
# --locked: every crate is the one Cargo.lock names, checksum included.
23+
RUN cargo build --release --locked \
24+
&& install -m 0755 target/release/durable-workflow-server /durable-workflow-server
25+
26+
# A digest-pinned slim runtime: glibc for the binary, nothing else. TLS uses
27+
# rustls with compiled-in web PKI roots, and SQLite is bundled.
28+
FROM mirror.gcr.io/library/debian@sha256:7ff6ba31527b9e336162fa061746c366c4b2032f4d2cff2078bea43f78efe3f8
29+
COPY --from=build /durable-workflow-server /usr/local/bin/durable-workflow-server
30+
# The PHP image's paths, so a taken-over installation keeps its volumes:
31+
# SQLite in /app/database, and external payload objects under /app/storage,
32+
# whose absolute file:// paths PHP records in the database. Native's default
33+
# local payload root is <working directory>/storage/app/external-payloads.
34+
RUN install -d -o 1000 -g 1000 -m 0750 /app /app/database /app/storage
35+
# Native runs the repair pass itself: this image has no separate scheduler
36+
# container, as published PHP's Compose deployment does.
37+
ENV DW_BIND_ADDRESS=0.0.0.0:8080 \
38+
DW_RUST_MAINTENANCE_LOOP=1 \
39+
DB_DATABASE=/app/database/database.sqlite
40+
USER 1000:1000
41+
WORKDIR /app
42+
VOLUME ["/app/database", "/app/storage"]
43+
EXPOSE 8080
44+
# Native stops gracefully on SIGTERM; PHP's image declares SIGWINCH.
45+
STOPSIGNAL SIGTERM
46+
ENTRYPOINT ["/usr/local/bin/durable-workflow-server"]
47+
LABEL org.opencontainers.image.title="Durable Workflow Server (native release candidate)" \
48+
org.opencontainers.image.source="https://github.com/durable-workflow/server" \
49+
org.opencontainers.image.licenses="MIT"

0 commit comments

Comments
 (0)