Skip to content

Server CI

Server CI #1592

Workflow file for this run

name: Server CI
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
merge_group:
permissions:
actions: read
contents: read
concurrency:
group: server-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
classify:
name: Select qualification depth
runs-on: ubuntu-24.04
outputs:
run_full: ${{ steps.depth.outputs.run_full }}
upstream_run_id: ${{ steps.depth.outputs.upstream_run_id }}
steps:
- id: depth
name: Reuse only an exact merge-queue qualification
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "$GITHUB_EVENT_NAME" == pull_request ]]; then
if jq -e 'any(.pull_request.labels[]?; .name == "ci:full")' \
"$GITHUB_EVENT_PATH" >/dev/null
then
echo 'run_full=true' >>"$GITHUB_OUTPUT"
else
echo 'run_full=false' >>"$GITHUB_OUTPUT"
fi
echo 'upstream_run_id=' >>"$GITHUB_OUTPUT"
exit 0
fi
if [[ "$GITHUB_EVENT_NAME" == merge_group ]]; then
echo 'run_full=true' >>"$GITHUB_OUTPUT"
echo 'upstream_run_id=' >>"$GITHUB_OUTPUT"
exit 0
fi
runs=$(gh api --method GET \
"repos/$GITHUB_REPOSITORY/actions/workflows/server-ci.yml/runs" \
--field head_sha="$GITHUB_SHA" --field event=merge_group \
--field status=completed)
upstream=$(jq -r --arg sha "$GITHUB_SHA" '
[.workflow_runs[] | select(
.head_sha == $sha and .event == "merge_group"
and .conclusion == "success"
)] | sort_by(.run_number) | last | .id // empty' <<<"$runs")
if [[ -n $upstream ]]; then
echo 'run_full=false' >>"$GITHUB_OUTPUT"
echo "upstream_run_id=$upstream" >>"$GITHUB_OUTPUT"
else
echo 'run_full=true' >>"$GITHUB_OUTPUT"
echo 'upstream_run_id=' >>"$GITHUB_OUTPUT"
fi
windows-filesystem:
name: Filesystem durability (${{ matrix.os }})
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [macos-15, windows-2025]
defaults:
run:
working-directory: mdbase-connect
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: mdbase-connect
- id: mdbase_revision
shell: bash
run: echo "revision=$(tr -d '\r\n' < deploy/docker/mdbase-rs-revision)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: callumalpass/mdbase-rs
ref: ${{ steps.mdbase_revision.outputs.revision }}
path: mdbase-rs
- name: Install Rust
shell: bash
run: |
rustup toolchain install 1.94.0 --profile minimal
rustup override set 1.94.0
- name: Restore Rust dependency cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: |
mdbase-connect -> target
key: filesystem-${{ matrix.os }}-${{ steps.mdbase_revision.outputs.revision }}
cache-on-failure: true
- run: cargo test --locked -p mdbase-connect-core -p mdbase-connect-daemon
env:
MDBASE_CONNECT_ENV: test
MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file
runtime-testbed:
name: Runtime interoperability testbed
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: mdbase-connect
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: mdbase-connect
- id: mdbase_revision
run: echo "revision=$(tr -d '\r\n' < deploy/docker/mdbase-rs-revision)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: callumalpass/mdbase-rs
ref: ${{ steps.mdbase_revision.outputs.revision }}
path: mdbase-rs
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: mdbase-dev/mdbase-spec
ref: 4a2525b221460425ede3b4652fe18b504dc367ec
path: mdbase-spec
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- run: rustup toolchain install 1.94.0 --profile minimal
- run: rustup override set 1.94.0
- name: Restore Rust dependency cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: |
mdbase-connect -> target
key: runtime-testbed-${{ steps.mdbase_revision.outputs.revision }}
cache-on-failure: true
- name: Install portable testbed runner
working-directory: mdbase-spec/packages/testbed
run: npm ci
- name: Exercise the Connect notification runtime
run: >-
node ../mdbase-spec/packages/testbed/src/cli.mjs run
--adapter command:cargo
--adapter-arg run
--adapter-arg --locked
--adapter-arg --quiet
--adapter-arg -p
--adapter-arg mdbase-connect-testbed-adapter
--adapter-arg --
--scenario runtime.application-execution
--timeout-ms 120000
node:
needs: classify
if: needs.classify.outputs.run_full == 'true' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 55
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.15.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm version:check
- run: pnpm check:release-readiness
- run: pnpm check:release-components
- run: pnpm audit:dependencies
- run: pnpm check:architecture
# Workspace packages publish their declarations from dist, so build the
# dependency graph before checking downstream consumers.
- run: pnpm build
# Browser-only provider boundaries run alongside the slower Rust and
# system suites instead of extending their critical path.
# `--with-deps` pulls system packages from distribution mirrors. On
# 2026-08-19 azure.archive.ubuntu.com served 21 MB of font packages at
# under 50 KB/s, hanging the step for 75 minutes before it was bounded.
#
# Cache the browser so most runs never contact a mirror at all, and keep
# the timeout so a cache miss on a slow day fails fast instead of
# silently consuming an hour.
- name: Restore Playwright cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: needs.classify.outputs.run_full == 'true'
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: "Cache marker: playwright hit"
if: needs.classify.outputs.run_full == 'true' && steps.playwright-cache.outputs.cache-hit == 'true'
run: echo 'Playwright cache hit.'
- name: "Cache marker: playwright miss"
if: needs.classify.outputs.run_full == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: echo 'Playwright cache miss.'
# The system dependencies are font packages that the runner image
# already carries; only a cache miss needs them refreshed. A miss must be
# able to finish even on a slow mirror, or the cache never populates and
# every run pays the same cost -- so this path gets a wider budget than
# the cached path. It is still bounded: the 2026-08-19 hang ran 75
# minutes unbounded.
- name: Install Chromium (cache miss)
if: needs.classify.outputs.run_full == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
timeout-minutes: 30
- name: Install Chromium (cache hit)
if: needs.classify.outputs.run_full == 'true' && steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install chromium
timeout-minutes: 5
- if: needs.classify.outputs.run_full == 'true'
run: pnpm test:browser-storage
- if: needs.classify.outputs.run_full == 'true'
run: pnpm test:accessibility
- run: pnpm typecheck
- run: pnpm test
- if: needs.classify.outputs.run_full == 'true'
run: pnpm package:audit
- name: Pack qualified public packages
if: needs.classify.outputs.run_full == 'true'
run: |
mkdir -p "$RUNNER_TEMP/npm-packages"
while IFS=$'\t' read -r package_name package_dir; do
pnpm --dir "$package_dir" pack \
--pack-destination "$RUNNER_TEMP/npm-packages"
done < <(node scripts/public-packages.mjs)
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: needs.classify.outputs.run_full == 'true'
with:
name: qualified-npm-packages
path: ${{ runner.temp }}/npm-packages/*.tgz
if-no-files-found: error
retention-days: 90
server-container-full:
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- run: docker build --file deploy/docker/Dockerfile.server --tag mdbase-connect-server:test .
- run: docker build --file deploy/docker/Dockerfile.nats --tag mdbase-connect-nats:test .
- run: >-
MDBASE_CONNECT_E2E_BUILD=0
node test/system/run.mjs --suite container --no-prepare
previous-release-upgrade:
name: Previous release upgrade and OAuth
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: mdbase_connect_upgrade
POSTGRES_USER: mdbase
POSTGRES_PASSWORD: previous-release-upgrade
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U mdbase -d mdbase_connect_upgrade"
--health-interval 5s
--health-timeout 3s
--health-retries 20
env:
DATABASE_URL: postgres://mdbase:previous-release-upgrade@127.0.0.1:5432/mdbase_connect_upgrade
CANDIDATE_IMAGE: mdbase-connect-server:upgrade-candidate
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Exercise the previous-server upgrade contract
run: test/upgrade/server-from-previous
previous-provider-upgrade:
name: Immediate-predecessor provider persisted-state upgrade
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_DB: mdbase_provider_upgrade
POSTGRES_USER: mdbase
POSTGRES_PASSWORD: previous-provider-upgrade
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U mdbase -d mdbase_provider_upgrade"
--health-interval 5s
--health-timeout 3s
--health-retries 20
env:
DATABASE_URL: postgres://mdbase:previous-provider-upgrade@127.0.0.1:5432/mdbase_provider_upgrade
CANDIDATE_IMAGE: mdbase-connect-hosted-provider:upgrade-candidate
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Exercise immediate-predecessor persisted-state recovery
run: test/upgrade/provider-from-previous
hosted-provider-rust:
name: Hosted provider Rust workspace
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: mdbase-connect
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: mdbase-connect
- id: mdbase_revision
run: echo "revision=$(tr -d '\r\n' < deploy/docker/mdbase-rs-revision)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: callumalpass/mdbase-rs
ref: ${{ steps.mdbase_revision.outputs.revision }}
path: mdbase-rs
- run: >-
rustup toolchain install 1.94.0
--profile minimal
--component rustfmt
--component clippy
- run: rustup override set 1.94.0
- run: cp deploy/docker/Cargo.lock.hosted-provider Cargo.lock
- name: Restore Rust dependency cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: |
mdbase-connect -> target
key: hosted-provider-${{ steps.mdbase_revision.outputs.revision }}
cache-on-failure: true
- run: cargo fmt --all --check
- run: cargo clippy --locked --workspace --all-targets -- -D warnings
- run: cargo test --locked --workspace
env:
MDBASE_CONNECT_ENV: test
MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file
- run: cargo build --locked --workspace
hosted-provider-system:
name: System suite (${{ matrix.shard }})
needs: classify
if: needs.classify.outputs.run_full == 'true'
runs-on: ubuntu-latest
timeout-minutes: ${{ matrix.timeout }}
strategy:
fail-fast: false
matrix:
include:
- shard: local-relay
suites: local,relay
browser: true
timeout: 55
- shard: sync
suites: sync
browser: false
timeout: 35
- shard: provider
suites: provider
browser: true
timeout: 55
- shard: files
suites: files
browser: false
timeout: 35
- shard: files-adversarial
suites: files-adversarial
browser: false
timeout: 35
- shard: desktop
suites: desktop
browser: true
timeout: 60
defaults:
run:
working-directory: mdbase-connect
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: mdbase-connect
- id: mdbase_revision
run: echo "revision=$(tr -d '\r\n' < deploy/docker/mdbase-rs-revision)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: callumalpass/mdbase-rs
ref: ${{ steps.mdbase_revision.outputs.revision }}
path: mdbase-rs
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.15.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: mdbase-connect/pnpm-lock.yaml
- run: >-
rustup toolchain install 1.94.0
--profile minimal
--component rustfmt
--component clippy
- run: rustup override set 1.94.0
- run: cp deploy/docker/Cargo.lock.hosted-provider Cargo.lock
- name: Restore Rust dependency cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: |
mdbase-connect -> target
key: hosted-provider-system-${{ matrix.shard }}-${{ steps.mdbase_revision.outputs.revision }}
cache-on-failure: true
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: cargo fmt --all --check
- run: pnpm check:cargo-features
- run: cargo clippy --locked --workspace --all-targets -- -D warnings
# Compile and test every Rust crate against the exact SDK revision used
# by the production provider image. This keeps path-only developer
# dependencies from making a clean checkout unreproducible.
- run: cargo test --locked --workspace
env:
MDBASE_CONNECT_ENV: test
MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file
- run: cargo build --locked --workspace
- name: Restore Playwright cache
if: matrix.browser
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('mdbase-connect/pnpm-lock.yaml') }}
- name: "Cache marker: playwright hit"
if: matrix.browser && steps.playwright-cache.outputs.cache-hit == 'true'
run: echo 'Playwright cache hit.'
- name: "Cache marker: playwright miss"
if: matrix.browser && steps.playwright-cache.outputs.cache-hit != 'true'
run: echo 'Playwright cache miss.'
- name: Install Chromium (cache miss)
if: matrix.browser && steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
timeout-minutes: 30
- name: Install Chromium (cache hit)
if: matrix.browser && steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install chromium
timeout-minutes: 5
- name: Run ${{ matrix.shard }} system suites
run: node test/system/run.mjs --suite "${{ matrix.suites }}" --no-prepare
env:
MDBASE_CONNECT_ENV: test
MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file
qualification:
name: Qualification
if: always()
needs:
- classify
- windows-filesystem
- runtime-testbed
- node
- server-container-full
- previous-release-upgrade
- previous-provider-upgrade
- hosted-provider-rust
- hosted-provider-system
runs-on: ubuntu-24.04
steps:
- name: Require the selected qualification lane
env:
RUN_FULL: ${{ needs.classify.outputs.run_full }}
UPSTREAM_RUN_ID: ${{ needs.classify.outputs.upstream_run_id }}
WINDOWS: ${{ needs.windows-filesystem.result }}
RUNTIME: ${{ needs.runtime-testbed.result }}
NODE: ${{ needs.node.result }}
CONTAINER: ${{ needs.server-container-full.result }}
SERVER_UPGRADE: ${{ needs.previous-release-upgrade.result }}
PROVIDER_UPGRADE: ${{ needs.previous-provider-upgrade.result }}
PROVIDER_RUST: ${{ needs.hosted-provider-rust.result }}
PROVIDER_SYSTEM: ${{ needs.hosted-provider-system.result }}
run: |
if [[ "$RUN_FULL" == true ]]; then
for result in "$WINDOWS" "$RUNTIME" "$NODE" "$CONTAINER" \
"$SERVER_UPGRADE" "$PROVIDER_UPGRADE" "$PROVIDER_RUST" \
"$PROVIDER_SYSTEM"
do
[[ $result == success ]]
done
elif [[ -z "$UPSTREAM_RUN_ID" ]]; then
[[ "$NODE" == success ]]
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Record immutable qualification inputs
env:
RUN_FULL: ${{ needs.classify.outputs.run_full }}
UPSTREAM_RUN_ID: ${{ needs.classify.outputs.upstream_run_id }}
run: |
kind=fast
[[ "$RUN_FULL" == true || -n "$UPSTREAM_RUN_ID" ]] && kind=full
tree=$(git rev-parse HEAD^{tree})
jq -n \
--arg commit "$GITHUB_SHA" \
--arg tree "$tree" \
--arg kind "$kind" \
--arg event "$GITHUB_EVENT_NAME" \
--arg run_id "$GITHUB_RUN_ID" \
--arg upstream_run_id "$UPSTREAM_RUN_ID" \
--arg pnpm_lock_sha256 "$(sha256sum pnpm-lock.yaml | cut -d' ' -f1)" \
--arg cargo_lock_sha256 "$(sha256sum deploy/docker/Cargo.lock.hosted-provider | cut -d' ' -f1)" \
--arg mdbase_rs_revision "$(tr -d '\r\n' <deploy/docker/mdbase-rs-revision)" \
--arg workflow_sha256 "$(sha256sum .github/workflows/server-ci.yml | cut -d' ' -f1)" \
'{schema_version:1,qualification:$kind,commit:$commit,tree:$tree,
event:$event,run_id:$run_id,
upstream_run_id:(if $upstream_run_id == "" then null else $upstream_run_id end),
inputs:{pnpm_lock_sha256:$pnpm_lock_sha256,
cargo_lock_sha256:$cargo_lock_sha256,
mdbase_rs_revision:$mdbase_rs_revision,
workflow_sha256:$workflow_sha256}}' \
>qualification.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: server-qualification
path: qualification.json
if-no-files-found: error
retention-days: 90