diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 0000000..d9fc005 --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,28 @@ +name: Setup Rust +description: Install pinned Rust and optional native dependencies, then restore the Rust cache. +inputs: + native: + description: Install native build dependencies. + default: "true" + cache-key: + description: Cache partition for this workload; empty disables caching. + default: "" +runs: + using: composite + steps: + - uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b # v3 + with: + just-version: 1.58.0 + - name: Install pinned Rust + shell: bash + run: rustup toolchain install --no-self-update --profile minimal --component rustfmt --component clippy + - name: Install native dependencies + if: inputs.native == 'true' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y clang libclang-dev pkg-config + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + if: inputs.cache-key != '' + with: + shared-key: ${{ inputs.cache-key }} diff --git a/.github/actions/setup-sdk/action.yml b/.github/actions/setup-sdk/action.yml new file mode 100644 index 0000000..b384aa2 --- /dev/null +++ b/.github/actions/setup-sdk/action.yml @@ -0,0 +1,18 @@ +name: Setup SDK tools +description: Install pinned Node, TypeScript and the prebuilt openapi-nexus binary. +runs: + using: composite + steps: + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24.15.0 + - name: Install TypeScript + shell: bash + run: | + npm install --prefix "$RUNNER_TEMP/sqlrest-typescript" --ignore-scripts typescript@6.0.3 + echo "$RUNNER_TEMP/sqlrest-typescript/node_modules/.bin" >> "$GITHUB_PATH" + - name: Download openapi-nexus binary + shell: bash + run: | + just download-openapi-nexus "$RUNNER_TEMP/openapi-nexus" + echo "OPENAPI_NEXUS_BIN=$RUNNER_TEMP/openapi-nexus/openapi-nexus" >> "$GITHUB_ENV" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bfd2a8..59406cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,57 +1,35 @@ name: contracts + on: push: + branches: [main] pull_request: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + quality: + uses: ./.github/workflows/quality.yml + tests: + uses: ./.github/workflows/tests.yml + sdk: + uses: ./.github/workflows/sdk.yml + package: + uses: ./.github/workflows/package.yml + + # A stable required check: failed, cancelled or skipped lanes cannot pass. delivery: - # The artifact must be compatible with the Docker runtime's glibc 2.36. + if: ${{ always() }} + needs: [quality, tests, sdk, package] runs-on: ubuntu-22.04 - timeout-minutes: 45 - services: - postgres: - image: postgres:18-alpine@sha256:54451ecb8ab38c24c3ec123f2fd501303a3a1856a5c66e98cecf2460d5e1e9d7 - env: - POSTGRES_PASSWORD: sqlrest - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U postgres" - --health-interval 5s - --health-timeout 5s - --health-retries 20 + timeout-minutes: 5 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24.15.0 - - name: Install pinned tools - run: | - rustup toolchain install 1.98.0 --profile minimal --component rustfmt --component clippy - sudo apt-get update - sudo apt-get install -y clang libclang-dev pkg-config postgresql-client curl xz-utils - npm install --prefix "$RUNNER_TEMP/sqlrest-typescript" --ignore-scripts typescript@6.0.3 - echo "$RUNNER_TEMP/sqlrest-typescript/node_modules/.bin" >> "$GITHUB_PATH" - - name: Download pinned openapi-nexus binary - run: | - bash scripts/download-openapi-nexus.sh "$RUNNER_TEMP/openapi-nexus" - echo "OPENAPI_NEXUS_BIN=$RUNNER_TEMP/openapi-nexus/openapi-nexus" >> "$GITHUB_ENV" - - name: Create empty example database - env: - PGPASSWORD: sqlrest - run: createdb -h 127.0.0.1 -U postgres sqlrest_examples - - name: Format, lint, unit, both backends, binary, restart and SDK contracts + - name: Require every lane to succeed env: - SQLREST_TEST_POSTGRES: postgresql://postgres:sqlrest@127.0.0.1:5432/postgres - SQLREST_EXAMPLES_POSTGRES: postgresql://postgres:sqlrest@127.0.0.1:5432/sqlrest_examples - run: bash scripts/check.sh - - name: Package verified release artifact and run container lifecycle - run: | - cargo build --release --locked - docker build --tag sqlrest:ci . - python3 scripts/e2e.py --image sqlrest:ci + RESULTS: ${{ toJSON(needs) }} + run: jq -e 'all(.[]; .result == "success")' <<< "$RESULTS" diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..30dee72 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,47 @@ +name: Package +on: + workflow_call: + +permissions: + contents: read + +jobs: + release: + name: Release binary + # Keep the build compatible with the container's glibc 2.36. + runs-on: ubuntu-22.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + cache-key: release + - run: just release + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: sqlrest-linux-x86_64 + path: target/release/sqlrest + if-no-files-found: error + retention-days: 7 + + container: + name: Container lifecycle + needs: [release] + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: sqlrest-linux-x86_64 + path: target/release + - name: Restore executable permission + run: chmod +x target/release/sqlrest + - uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b # v3 + with: + just-version: 1.58.0 + - run: just container diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..527bdb5 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,33 @@ +name: Quality +on: + workflow_call: + +permissions: + contents: read + +jobs: + fmt: + name: Format + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + native: "false" + - run: just fmt + + clippy: + name: Clippy + runs-on: ubuntu-22.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + cache-key: clippy + - run: just clippy diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml new file mode 100644 index 0000000..14cf383 --- /dev/null +++ b/.github/workflows/sdk.yml @@ -0,0 +1,42 @@ +name: SDK contracts +on: + workflow_call: + +permissions: + contents: read + +jobs: + sdk: + name: ${{ matrix.contract }} + runs-on: ubuntu-22.04 + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + contract: [turso, postgres, schema] + services: + postgres: + image: ${{ matrix.contract == 'postgres' && 'postgres:18-alpine@sha256:54451ecb8ab38c24c3ec123f2fd501303a3a1856a5c66e98cecf2460d5e1e9d7' || '' }} + env: + POSTGRES_PASSWORD: sqlrest + POSTGRES_DB: sqlrest_examples + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + cache-key: tests + - uses: ./.github/actions/setup-sdk + - name: Run SDK contract + env: + SQLREST_EXAMPLES_POSTGRES: postgresql://postgres:sqlrest@127.0.0.1:5432/sqlrest_examples + CHECK_GROUP: sdk-${{ matrix.contract }} + run: just "$CHECK_GROUP" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5feecd6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Database contracts +on: + workflow_call: + +permissions: + contents: read + +jobs: + contracts: + name: ${{ matrix.backend }} + runs-on: ubuntu-22.04 + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + backend: [turso, postgres] + services: + postgres: + image: ${{ matrix.backend == 'postgres' && 'postgres:18-alpine@sha256:54451ecb8ab38c24c3ec123f2fd501303a3a1856a5c66e98cecf2460d5e1e9d7' || '' }} + env: + POSTGRES_PASSWORD: sqlrest + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + cache-key: tests + - name: Run database contracts + env: + SQLREST_TEST_POSTGRES: postgresql://postgres:sqlrest@127.0.0.1:5432/postgres + CHECK_GROUP: ${{ matrix.backend }} + run: just "$CHECK_GROUP" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..2f2593a --- /dev/null +++ b/Justfile @@ -0,0 +1,62 @@ +set shell := ["bash", "-euo", "pipefail", "-c"] + +# Run the complete local contract gates (release/container are separate). +default: all + +all: _all-inputs fmt clippy turso postgres sdk-postgres sdk-turso sdk-schema + +[private] +_all-inputs: + : "${SQLREST_TEST_POSTGRES:?Set a disposable PostgreSQL URL for core contracts}" + : "${SQLREST_EXAMPLES_POSTGRES:?Set a DIFFERENT empty PostgreSQL database URL for examples}" + : "${OPENAPI_NEXUS_BIN:?Set the pinned openapi-nexus binary path}" + test "$SQLREST_TEST_POSTGRES" != "$SQLREST_EXAMPLES_POSTGRES" || { echo "Core and example database URLs must be different" >&2; exit 2; } + +# Check Rust formatting and whitespace. +fmt: + just --fmt --check + cargo fmt --check + git diff --check + +clippy: + cargo clippy --locked --all-targets -- -D warnings + +# Includes backend-neutral contracts, doctests and real Turso. +turso: + cargo test --locked + +# Never run blanket --ignored: restart_worker is a subprocess fixture and +# the old upstream high-level Turso CPU timeout probe is intentionally skipped. +postgres: + : "${SQLREST_TEST_POSTGRES:?Set a disposable PostgreSQL URL for core contracts}" + cargo test --locked --lib postgres_tests:: -- --ignored + cargo test --locked --test migration_contract postgres_ -- --ignored + cargo test --locked --test postgres_contract --test execution_contract --test commit_contract --test registry_postgres -- --ignored + cargo test --locked --test http_contract postgres_http_lifecycle -- --ignored + +sdk-turso: + : "${OPENAPI_NEXUS_BIN:?Set the pinned openapi-nexus binary path}" + cargo test --locked --test examples_contract generated_sdks_call_real_examples -- --ignored + +sdk-postgres: + : "${SQLREST_EXAMPLES_POSTGRES:?Set an empty disposable PostgreSQL database URL for examples}" + : "${OPENAPI_NEXUS_BIN:?Set the pinned openapi-nexus binary path}" + SQLREST_TEST_POSTGRES="$SQLREST_EXAMPLES_POSTGRES" cargo test --locked --test examples_contract postgres_examples_and_process_restart -- --ignored + +sdk-schema: + : "${OPENAPI_NEXUS_BIN:?Set the pinned openapi-nexus binary path}" + cargo test --locked --test sdk_contract -- --ignored + +release: + cargo build --release --locked + +# Package an existing release binary and exercise its container lifecycle. +container: + docker build --tag sqlrest:ci . + python3 scripts/e2e.py --image sqlrest:ci + +# Download the pinned Linux x86_64 musl binary; never build the generator. +download-openapi-nexus destination: + mkdir -p -- {{ quote(destination) }} + curl -fsSL https://github.com/rust-codegen-group/openapi-nexus/releases/download/0.2.3/openapi-nexus-x86_64-unknown-linux-musl.tar.xz | tar -xJ --strip-components=1 -C {{ quote(destination) }} + {{ quote(destination + "/openapi-nexus") }} --version diff --git a/docs/delivery.md b/docs/delivery.md index 2da814a..b8f9c23 100644 --- a/docs/delivery.md +++ b/docs/delivery.md @@ -14,7 +14,7 @@ PostgreSQL **18.3** from pinned `postgres:18-alpine` digest `sha256:54451ecb8ab38c24c3ec123f2fd501303a3a1856a5c66e98cecf2460d5e1e9d7`, and uses Node **24.15.0**, TypeScript **6.0.3**, and the openapi-nexus **0.2.3** release binary. CI and local SDK verification use -`scripts/download-openapi-nexus.sh` to download and extract the Linux x86_64 musl +`just download-openapi-nexus DESTINATION` to download and extract the Linux x86_64 musl archive. No generator source is cloned or compiled; download or extraction errors fail the setup. @@ -83,19 +83,21 @@ opt-in dependencies, **not evidence of passing** when reported ignored. Provision two disposable PG databases: a core-test database and a separate **empty** examples database. The latter is populated by the examples and must be -new for another run. Then: +new for another run. Install Just **1.58.0** first (CI installs this version). +Then: ```sh SQLREST_TOOLS=$(mktemp -d) -bash scripts/download-openapi-nexus.sh "$SQLREST_TOOLS/openapi-nexus" +just download-openapi-nexus "$SQLREST_TOOLS/openapi-nexus" SQLREST_TEST_POSTGRES='postgresql://user:password@host/core_test' \ SQLREST_EXAMPLES_POSTGRES='postgresql://user:password@host/examples_test' \ OPENAPI_NEXUS_BIN="$SQLREST_TOOLS/openapi-nexus/openapi-nexus" \ - bash scripts/check.sh + just ``` -The script requires all inputs and fails on missing dependencies/errors. It runs +`just --list` lists the available tasks. The default `all` recipe requires all +inputs and fails on missing dependencies/errors. It runs format, Clippy, normal tests, selected PG migration/execution/HTTP tests, both-backend SDK HTTP examples, and the recursive SDK compile test. It does not delete the databases. Never use production or user-data databases. @@ -105,9 +107,49 @@ subprocess fixture, and the upstream high-level Turso CPU timeout probe is intentionally skipped. The actual SDK-driver deadline path has separate passing tests; skipping the old probe does not mean it was repaired. -The CI workflow runs these gates, builds the release artifact, builds the image -and invokes the container restart tutorial. No remote CI run or release is implied -by checking in a workflow; local verification and hosted CI are distinct evidence. +Each group can also run independently without unrelated dependencies: + +```sh +just fmt +just clippy +just turso +SQLREST_TEST_POSTGRES='postgresql://user:password@host/core_test' \ + just postgres +``` + +SDK groups are `sdk-turso`, `sdk-postgres` and `sdk-schema`. All need +`OPENAPI_NEXUS_BIN` and the SDK tools; only `sdk-postgres` needs +`SQLREST_EXAMPLES_POSTGRES`, pointing to its own empty database. +`release` builds the binary; `container` packages an existing release binary and +runs its lifecycle tests. These two remain separate from the default `all` group. + +## Parallel CI + +`.github/workflows/ci.yml` orchestrates four reusable workflows: + +| Workflow | Parallel jobs | Dependencies | +| --- | --- | --- | +| `quality.yml` | Format, Clippy | Rust; native tools/cache only for Clippy | +| `tests.yml` | Turso/default contracts, PostgreSQL contracts | PostgreSQL service only for its matrix entry | +| `sdk.yml` | Turso SDK, PostgreSQL SDK, recursive schema | SDK tools; separate empty PostgreSQL service only for PG | +| `package.yml` | Release binary, then container lifecycle | Container downloads the binary built in the same run | + +The four workflows start independently. Database and SDK matrices use +`fail-fast: false` so a failure does not cancel other diagnostic results. +Composite actions in `.github/actions/` share Rust/native/cache setup and SDK +tool installation. Rust comes from `rust-toolchain.toml`; Clippy, tests and release +use separate cache partitions. SDK jobs share the tests partition. +The artifact is a candidate until the complete CI run succeeds; it is not a +published release. + +The final `delivery` job always evaluates all four results and passes only when +all succeeded, including container verification. Use it as the aggregate required +check; a failed, cancelled or skipped dependency cannot turn it green. +PR updates cancel obsolete runs. Push builds run only on `main`, avoiding duplicate +push and pull-request runs for the same feature branch. + +No remote CI run or release is implied by checking in a workflow; local validation +and hosted CI are distinct evidence. ## Embedding, backups and trust diff --git a/examples/sdk/README.md b/examples/sdk/README.md index aa8ed97..6c3fa61 100644 --- a/examples/sdk/README.md +++ b/examples/sdk/README.md @@ -17,7 +17,7 @@ use an existing matching installation; put its `tsc` on PATH. ```sh SQLREST_TOOLS=$(mktemp -d) -bash scripts/download-openapi-nexus.sh "$SQLREST_TOOLS/openapi-nexus" +just download-openapi-nexus "$SQLREST_TOOLS/openapi-nexus" cargo build --locked OPENAPI_NEXUS_BIN="$SQLREST_TOOLS/openapi-nexus/openapi-nexus" \ python3 scripts/e2e.py --sdk diff --git a/scripts/check.sh b/scripts/check.sh deleted file mode 100644 index be5dc38..0000000 --- a/scripts/check.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "$0")/.." - -# Explicit opt-in dependencies; never silently skip release gates. -: "${SQLREST_TEST_POSTGRES:?Set a disposable PostgreSQL URL for core contracts}" -: "${SQLREST_EXAMPLES_POSTGRES:?Set a DIFFERENT empty PostgreSQL database URL for examples}" -: "${OPENAPI_NEXUS_BIN:?Set the pinned openapi-nexus binary path}" -if [[ "$SQLREST_TEST_POSTGRES" == "$SQLREST_EXAMPLES_POSTGRES" ]]; then - echo "Core and example database URLs must be different" >&2 - exit 2 -fi - -cargo fmt --check -cargo clippy --locked --all-targets -- -D warnings -cargo test --locked -# PostgreSQL-dependent library tests belong in postgres_tests modules. -cargo test --locked --lib postgres_tests:: -- --ignored -cargo test --locked --test migration_contract postgres_ -- --ignored -cargo test --locked --test postgres_contract --test execution_contract \ - --test commit_contract --test registry_postgres -- --ignored -cargo test --locked --test http_contract postgres_http_lifecycle -- --ignored -SQLREST_TEST_POSTGRES="$SQLREST_EXAMPLES_POSTGRES" \ - cargo test --locked --test examples_contract postgres_examples_and_process_restart -- --ignored -cargo test --locked --test examples_contract generated_sdks_call_real_examples -- --ignored -cargo test --locked --test sdk_contract -- --ignored -git diff --check -# Do not run blanket --ignored: restart_worker is a fixture-only subprocess, -# and the old upstream high-level Turso CPU timeout probe is intentionally skipped. diff --git a/scripts/download-openapi-nexus.sh b/scripts/download-openapi-nexus.sh deleted file mode 100644 index ffd316e..0000000 --- a/scripts/download-openapi-nexus.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -destination=${1:?Usage: bash scripts/download-openapi-nexus.sh DESTINATION} -version=0.2.3 -asset=openapi-nexus-x86_64-unknown-linux-musl - -mkdir -p -- "$destination" -curl -fsSL "https://github.com/rust-codegen-group/openapi-nexus/releases/download/$version/$asset.tar.xz" \ - | tar -xJ --strip-components=1 -C "$destination" -"$destination/openapi-nexus" --version