Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/native-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Native image

# Builds the native (Rust) server image from Dockerfile.native and runs the
# same smoke test the native release runs (Server #325). Only changes to the
# image's own files run it: the release workflow is dispatch-only, so this is
# where a pull request finds out that the Dockerfile, its ignore file or the
# smoke and publish scripts no longer work. Nothing is pushed.

on:
pull_request:
paths:
- Dockerfile.native
- Dockerfile.native.dockerignore
- scripts/ci/native-image-smoke.sh
- scripts/ci/native-release-publish.sh
- .github/workflows/native-image.yml
- .github/workflows/native-release.yml

permissions:
contents: read

concurrency:
group: native-image-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
smoke:
name: Build and smoke the native image (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: ubuntu-latest
- platform: linux/arm64
arch: arm64
runner: ubuntu-24.04-arm
env:
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
steps:
- name: Check out
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Build the image into the local daemon
timeout-minutes: 45
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: Dockerfile.native
platforms: ${{ matrix.platform }}
pull: true
provenance: false
load: true
push: false
tags: ${{ env.RESOURCE_SCOPE }}:smoke

- name: Smoke-test the image
env:
PLATFORM: ${{ matrix.platform }}
ARCH: ${{ matrix.arch }}
run: bash scripts/ci/native-image-smoke.sh "$RESOURCE_SCOPE:smoke" "$PLATFORM" "native-image/smoke-$ARCH.json"

- name: Remove this job's smoke containers and network
if: always()
run: |
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
docker network rm "$RESOURCE_SCOPE" || true

- name: Keep the smoke receipt
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: native-image-smoke-${{ matrix.arch }}
path: native-image
retention-days: 7
if-no-files-found: ignore
265 changes: 265 additions & 0 deletions .github/workflows/native-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
name: Native release candidate

# Publishes the native (Rust) server image for an existing immutable release
# tag (Server #325). Each architecture builds on its own runner and pushes by
# digest only; that exact digest is pulled back and smoke-tested there, so a
# failed smoke leaves no tag. The publish job then refuses tags that already
# exist, gives both smoked digests
# the release tag in both registries as one index, verifies it, and attests
# its build provenance. The PHP image and its Release workflow are unchanged.

run-name: Native ${{ inputs.tag }} at ${{ inputs.release_commit }}

on:
workflow_dispatch:
inputs:
tag:
description: 'Existing immutable release tag to publish natively (for example 2.12.0-rc.1)'
required: true
release_commit:
description: 'Full source commit of that tag'
required: true

permissions:
contents: read

concurrency:
group: native-release-${{ inputs.tag }}
cancel-in-progress: false

env:
DOCKERHUB_IMAGE: durableworkflow/server-native
GHCR_IMAGE: ghcr.io/durable-workflow/server-native

jobs:
build-amd64:
name: Build and smoke linux/amd64
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 75
environment: dockerhub
permissions:
contents: read
packages: write
env:
PLATFORM: linux/amd64
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out trusted release tooling
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.sha }}

- name: Check out immutable release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
path: release-source
ref: refs/tags/${{ inputs.tag }}

- name: Verify the release tag names the planned commit
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
run: |
set -euo pipefail
scripts/ci/verify-release-tag-source.sh
test "$(git -C release-source rev-parse HEAD)" = "$RELEASE_COMMIT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push linux/amd64 by digest only
id: build
timeout-minutes: 60
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: release-source
file: release-source/Dockerfile.native
platforms: linux/amd64
pull: true
provenance: false
labels: |
org.opencontainers.image.revision=${{ inputs.release_commit }}
dev.durable-workflow.release.tag=${{ inputs.tag }}
dev.durable-workflow.release.run-id=${{ github.run_id }}
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true

- name: Smoke-test the pushed linux/amd64 digest on its own architecture
env:
IMAGE: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
run: bash scripts/ci/native-image-smoke.sh "$IMAGE" "$PLATFORM" native-release/smoke-amd64.json

- name: Remove this job's smoke containers and network
if: always()
run: |
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
docker network rm "$RESOURCE_SCOPE" || true

- name: Keep the linux/amd64 smoke receipt
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: native-smoke-amd64
path: native-release
retention-days: 30
if-no-files-found: ignore

build-arm64:
name: Build and smoke linux/arm64
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04-arm
timeout-minutes: 75
environment: dockerhub
permissions:
contents: read
packages: write
env:
PLATFORM: linux/arm64
RESOURCE_SCOPE: dw-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out trusted release tooling
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.sha }}

- name: Check out immutable release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
path: release-source
ref: refs/tags/${{ inputs.tag }}

- name: Verify the release tag names the planned commit
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
run: |
set -euo pipefail
scripts/ci/verify-release-tag-source.sh
test "$(git -C release-source rev-parse HEAD)" = "$RELEASE_COMMIT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push linux/arm64 by digest only
id: build
timeout-minutes: 60
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: release-source
file: release-source/Dockerfile.native
platforms: linux/arm64
pull: true
provenance: false
labels: |
org.opencontainers.image.revision=${{ inputs.release_commit }}
dev.durable-workflow.release.tag=${{ inputs.tag }}
dev.durable-workflow.release.run-id=${{ github.run_id }}
outputs: type=image,"name=${{ env.GHCR_IMAGE }},${{ env.DOCKERHUB_IMAGE }}",push-by-digest=true,name-canonical=true,push=true

- name: Smoke-test the pushed linux/arm64 digest on its own architecture
env:
IMAGE: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }}
run: bash scripts/ci/native-image-smoke.sh "$IMAGE" "$PLATFORM" native-release/smoke-arm64.json

- name: Remove this job's smoke containers and network
if: always()
run: |
docker rm -f "$RESOURCE_SCOPE-native-sqlite" "$RESOURCE_SCOPE-native-pgsql" "$RESOURCE_SCOPE-native-mysql" "$RESOURCE_SCOPE-postgres" "$RESOURCE_SCOPE-mysql" || true
docker network rm "$RESOURCE_SCOPE" || true

- name: Keep the linux/arm64 smoke receipt
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: native-smoke-arm64
path: native-release
retention-days: 30
if-no-files-found: ignore

publish:
name: Tag, verify and attest the native index
needs: [build-amd64, build-arm64]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
environment: dockerhub
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Check out trusted release tooling
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.sha }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Tag and verify the index in both registries
id: index
env:
RELEASE_TAG: ${{ inputs.tag }}
DIGEST_AMD64: ${{ needs.build-amd64.outputs.digest }}
DIGEST_ARM64: ${{ needs.build-arm64.outputs.digest }}
run: bash scripts/ci/native-release-publish.sh

- name: Attest the index's build provenance in GHCR
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.index.outputs.digest }}
push-to-registry: true

- name: Attest the index's build provenance in Docker Hub
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: index.docker.io/${{ env.DOCKERHUB_IMAGE }}
subject-digest: ${{ steps.index.outputs.digest }}
push-to-registry: true
49 changes: 49 additions & 0 deletions Dockerfile.native
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Durable Workflow native (Rust) server, published as a release candidate
# (Server #325). It is opt-in: the server refuses to start without
# DW_RUST_EXPERIMENTAL=1. The published PHP image remains the default.
#
# The same image runs the takeover commands an operator uses to move a
# stopped, backed-up PHP database to native: upgrade-inspect,
# upgrade-queue-inspect, upgrade-payload-inspect, upgrade-fingerprint,
# upgrade-prepare-sqlite and upgrade-takeover
# (docs/rust-server-upgrade-takeover.md).

# Rust 1.99.0 on Debian 12 (glibc 2.36, matching the runtime below), pinned
# as its multi-architecture index so each architecture builds natively:
# linux/amd64 b36c246742b4…, linux/arm64 e393e07b3c3c… (Security verified
# the index, server#803 comment 6434047541).
FROM mirror.gcr.io/library/rust@sha256:114c7a4425406451c2866b6aafe69fe29b1b298832db1277d411ac73c82d04d6 AS build
ARG CARGO_BUILD_JOBS=2
ENV CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS} \
CARGO_INCREMENTAL=0 \
CARGO_PROFILE_RELEASE_DEBUG=0
WORKDIR /source/rust
COPY rust/ /source/rust/
# --locked: every crate is the one Cargo.lock names, checksum included.
RUN cargo build --release --locked \
&& install -m 0755 target/release/durable-workflow-server /durable-workflow-server

# A digest-pinned slim runtime: glibc for the binary, nothing else. TLS uses
# rustls with compiled-in web PKI roots, and SQLite is bundled.
FROM mirror.gcr.io/library/debian@sha256:7ff6ba31527b9e336162fa061746c366c4b2032f4d2cff2078bea43f78efe3f8
COPY --from=build /durable-workflow-server /usr/local/bin/durable-workflow-server
# The PHP image's paths, so a taken-over installation keeps its volumes:
# SQLite in /app/database, and external payload objects under /app/storage,
# whose absolute file:// paths PHP records in the database. Native's default
# local payload root is <working directory>/storage/app/external-payloads.
RUN install -d -o 1000 -g 1000 -m 0750 /app /app/database /app/storage
# Native runs the repair pass itself: this image has no separate scheduler
# container, as published PHP's Compose deployment does.
ENV DW_BIND_ADDRESS=0.0.0.0:8080 \
DW_RUST_MAINTENANCE_LOOP=1 \
DB_DATABASE=/app/database/database.sqlite
USER 1000:1000
WORKDIR /app
VOLUME ["/app/database", "/app/storage"]
EXPOSE 8080
# Native stops gracefully on SIGTERM; PHP's image declares SIGWINCH.
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/local/bin/durable-workflow-server"]
LABEL org.opencontainers.image.title="Durable Workflow Server (native release candidate)" \
org.opencontainers.image.source="https://github.com/durable-workflow/server" \
org.opencontainers.image.licenses="MIT"
Loading
Loading