fix(partiql): UPDATE on a non-existent key fails ConditionalCheckFailed #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |