name: CI
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
name: Test (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: default
flags: ""
- name: minimal-library
flags: "--no-default-features --features native-sqlite --lib"
- name: encryption-only
flags: "--no-default-features --features encryption --lib"
- name: encryption+http-server
flags: "--no-default-features --features encryption,http-server"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build ${{ matrix.flags }}
- name: Test
run: cargo test ${{ matrix.flags }}
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- name: Clippy (default)
run: cargo clippy --all-targets -- -D warnings
- name: Clippy (encryption)
run: cargo clippy --no-default-features --features encryption --lib -- -D warnings
- name: Format
run: cargo fmt --check
cargo-deny:
name: cargo-deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
feature-guards:
name: Feature guard checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Both features must fail
run: |
if cargo check --features "native-sqlite,encryption" 2>&1; then
echo "ERROR: both features should cause compile_error!"
exit 1
fi
- name: Neither feature must fail
run: |
if cargo check --no-default-features --features http-server 2>&1; then
echo "ERROR: no SQLite backend should cause compile_error!"
exit 1
fi
wasm-check:
name: wasm-sqlite build check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: cargo check (wasm32, wasm-sqlite)
run: cargo check --target wasm32-unknown-unknown --no-default-features --features wasm-sqlite --lib
- name: cargo check (wasm32, wasm-harness)
run: cargo check --target wasm32-unknown-unknown --no-default-features --features wasm-harness --lib
engine-client:
name: Engine client tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install npm dependencies
run: npm ci
- name: Test the engine client
run: node --test js/*.test.js
- name: Check CONTRACT_VERSION agreement
run: scripts/check-contract-version.sh
browser-engine:
name: Browser engine tests (real SQLite-wasm + OPFS)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Install wasm-pack
uses: taiki-e/install-action@v2
with:
tool: wasm-pack
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install npm dependencies
run: npm ci
- name: Build the wasm bundle
run: npm run build:wasm
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Run the browser engine tests
run: npm run test:browser
changes:
name: Detect recipe-affecting changes
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changes
id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# workflow_dispatch has no PR base, and a missing/unreachable base ref
# should fail open — run the smoke job rather than skip it silently.
if [ -z "${BASE_SHA:-}" ] || ! changed=$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null); then
echo "docker=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Changed files:"; echo "$changed"
if echo "$changed" | grep -qE '^(Dockerfile|Cargo\.(toml|lock)|src/main\.rs|src/mcp/|README\.md|\.github/workflows/ci\.yml)'; then
echo "docker=true" >> "$GITHUB_OUTPUT"
else
echo "docker=false" >> "$GITHUB_OUTPUT"
fi
docker-smoke:
name: Docker MCP smoke test
needs: changes
if: needs.changes.outputs.docker == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
- name: Install musl tools
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Build static musl binary
run: cargo build --release --target x86_64-unknown-linux-musl --features full
- name: Stage binary into the Docker build context
run: |
set -euo pipefail
mkdir -p dist/amd64
cp target/x86_64-unknown-linux-musl/release/dynoxide dist/amd64/dynoxide
chmod +x dist/amd64/dynoxide
file dist/amd64/dynoxide
# musl builds report "static-pie linked"; zig cross-builds report
# "statically linked". Both are self-contained and run on FROM scratch.
file dist/amd64/dynoxide | grep -qE 'static-pie linked|statically linked'
- uses: docker/setup-buildx-action@v3
- name: Build image (linux/amd64)
run: docker buildx build --provenance=false --platform linux/amd64 --load -t dynoxide:smoke .
- name: Image declares the MCP port (R1)
run: |
docker image inspect dynoxide:smoke \
--format '{{json .Config.ExposedPorts}}' | tee /dev/stderr | grep -q '19280/tcp'
- name: Start the documented DynamoDB + MCP recipe
env:
DYNOXIDE_MCP_AUTH_TOKEN: ci-smoke-test-token
run: |
set -euo pipefail
docker run -d --name dynoxide-smoke \
-p 127.0.0.1:8000:8000 -p 127.0.0.1:19280:19280 \
-e DYNOXIDE_MCP_AUTH_TOKEN \
dynoxide:smoke \
serve --host 0.0.0.0 --port 8000 \
--mcp --mcp-host 0.0.0.0 --mcp-port 19280
- name: DynamoDB reachable on :8000 (R3)
run: |
set -euo pipefail
ok=
for _ in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8000/ | grep -q 'healthy'; then
echo "DynamoDB up"; ok=1; break
fi
sleep 1
done
[ "${ok:-}" = 1 ] || { echo "::error::DynamoDB never came up"; docker logs dynoxide-smoke; exit 1; }
- name: MCP authenticated-reachable on :19280 (R2, R4)
run: |
set -euo pipefail
body='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}}}'
resp=$(curl -fsS -X POST http://127.0.0.1:19280/mcp \
-H 'Authorization: Bearer ci-smoke-test-token' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "$body")
echo "$resp"
echo "$resp" | grep -q 'dynoxide'
- name: MCP rejects a missing token with 401 (R4)
run: |
set -euo pipefail
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:19280/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}')
echo "status=$code"
[ "$code" = 401 ] || { echo "::error::expected 401 without token, got $code"; docker logs dynoxide-smoke; exit 1; }
- name: Teardown
if: always()
run: docker rm -f dynoxide-smoke || true