Skip to content

fix(partiql): UPDATE on a non-existent key fails ConditionalCheckFailed #113

fix(partiql): UPDATE on a non-existent key fails ConditionalCheckFailed

fix(partiql): UPDATE on a non-existent key fails ConditionalCheckFailed #113

Workflow file for this run

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@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build ${{ matrix.flags }}
- name: Test
run: cargo test ${{ matrix.flags }}
# The listener bind path is platform-specific (SO_REUSEADDR on Unix,
# SO_EXCLUSIVEADDRUSE on Windows), so the unit tests need a Windows run;
# test-build.yml only builds there.
test-windows:
name: Test (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test --lib
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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@v5
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
feature-guards:
name: Feature guard checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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@v5
- 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
# The harness exercises WasmDatabase and the action types directly, so it
# catches trait or re-export drift the wasm-sqlite check alone misses.
- 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@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
# The EngineClient tests run against a stub worker, but the bridge unit
# test loads the real @sqlite.org/sqlite-wasm engine over an in-memory
# database, so the job installs npm deps (no wasm build needed) and runs
# node --test.
- name: Install npm dependencies
run: npm ci
- name: Test the engine client
run: node --test js/*.test.js
# Fast guard that the client's CONTRACT_VERSION matches the Rust engine's,
# without building wasm; build-wasm.sh runs the same check before publish.
- 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@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
# wasm-pack drives the wasm-bindgen build inside scripts/build-wasm.sh.
- name: Install wasm-pack
uses: taiki-e/install-action@v2
with:
tool: wasm-pack
- uses: actions/setup-node@v5
with:
node-version: "22"
- name: Install npm dependencies
run: npm ci
# Build the same self-contained bundle a consumer installs, on the release
# profile, so the full --minify path (renamed identifiers, the worker that
# actually ships) is exercised against real Chromium + OPFS on every PR -
# not only at publish time and in a manual run. This build also runs the
# harness-op strip guard (build-wasm.sh step 2b) against the minified
# output. The wasm-opt cost is the price of testing what ships; the dev
# profile (build:wasm:dev) only minifies syntax and would leave the
# identifier-renaming transform unverified.
- name: Build the wasm bundle
run: npm run build:wasm
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
# Drives the shipped Worker in headless Chromium against the real SQLite-wasm engine and
# OPFS - the path the conformance suite (native backend) does not cover.
- name: Run the browser engine tests
run: npm run test:browser
# Gate the Docker smoke test on the paths that can actually break the
# documented MCP-in-Docker recipe. src/main.rs carries the ServeArgs/McpArgs
# flag plumbing and Cargo.{toml,lock} the feature set / rmcp version — both can
# break the recipe without touching src/mcp/, so a src/mcp-only gate is too
# narrow. Implemented with git diff (no third-party action).
changes:
name: Detect recipe-affecting changes
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- uses: actions/checkout@v5
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@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
# FROM scratch needs a fully static binary; native-sqlite bundles SQLite
# via a C compiler, so the musl C toolchain (musl-gcc) is required. This
# mirrors the release-amd64 build (.github/workflows/release.yml).
- 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@v4
# Two flags are load-bearing for `--load`:
# --platform linux/amd64 — without it buildx builds a multi-platform
# manifest that --load cannot export.
# --provenance=false — without it buildx attaches an attestation
# manifest, which is also a manifest list and is rejected by --load on
# the runner's classic (non-containerd) image store.
# Single-arch amd64 is enough for a smoke test; release.yml does multi-arch.
- 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'
# The documented recipe. The token is a hardcoded throwaway, intentionally
# inline (not a repository secret): it has no value outside this job. The
# host-side bind is pinned to 127.0.0.1 so the ports are not reachable on
# the Actions Docker bridge.
- 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
# Poll the port directly; the container HEALTHCHECK's 30s interval lags a
# fast smoke run. GET / is the same liveness probe `dynoxide healthcheck`
# uses (an unsigned DynamoDB op would 400 on SigV4 auth).
- 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