name: CI on: push: tags: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 branches: - "main" paths-ignore: - "docs/**" - "README.md" - ".github/workflows/release.yml" - ".github/workflows/official-site.yml" pull_request: branches: - "main" paths-ignore: - "docs/**" - "README.md" - ".github/workflows/release.yml" - ".github/workflows/official-site.yml" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_TERM_COLOR: always HURL_VERSION: 8.0.0 REGISTRY_USERNAME: lovasoa REGISTRY_IMAGE: lovasoa/sqlpage jobs: compile_and_lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm ci - run: npm test - name: Set up cargo cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 env: NODE_OPTIONS: --no-deprecation with: shared-key: rust-sqlpage-proj-test save-if: ${{ github.ref == 'refs/heads/main' }} - run: cargo fmt --all -- --check - run: cargo clippy --all-targets --all-features -- -D warnings # The database matrix below runs the same Rust test harnesses against # different DATABASE_URL values. Package the Linux test executables once # here so those jobs do not recompile SQLPage or its dependencies. - name: Package Linux Rust test binaries run: | set -euo pipefail rm -rf target/sqlpage-test-binaries mkdir -p target/sqlpage-test-binaries cargo test --features odbc-static --no-run --message-format=json \ | jq -r 'select(.profile.test == true and .executable != null) | .executable' \ | while IFS= read -r test_binary; do cp -- "$test_binary" target/sqlpage-test-binaries/ done test -n "$(find target/sqlpage-test-binaries -maxdepth 1 -type f -print -quit)" tar -C target/sqlpage-test-binaries -czf target/sqlpage-linux-test-binaries.tar.gz . - name: Build Linux binary run: cargo build --features odbc-static - name: Upload Linux Rust test binaries uses: actions/upload-artifact@v7 with: name: sqlpage-linux-test-binaries path: "target/sqlpage-linux-test-binaries.tar.gz" - name: Upload Linux binary uses: actions/upload-artifact@v7 with: name: sqlpage-linux-debug path: "target/debug/sqlpage" test: runs-on: ubuntu-latest needs: compile_and_lint strategy: matrix: include: - database: sqlite container: "" db_url: "sqlite::memory:" - database: postgres container: postgres db_url: "postgres://root:Password123!@127.0.0.1/sqlpage" - database: mysql container: mysql db_url: "mysql://root:Password123!@127.0.0.1/sqlpage" - database: mssql container: mssql db_url: "mssql://root:Password123!@127.0.0.1/sqlpage" - database: odbc container: postgres db_url: "Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!" setup_odbc: true - database: oracle container: oracle db_url: "Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!" steps: - uses: actions/checkout@v6 # Reuse the exact Linux test harnesses produced by compile_and_lint. # This keeps the DB matrix focused on database behavior instead of # compiling the same Rust crate five more times. - name: Download Linux Rust test binaries uses: actions/download-artifact@v8 with: name: sqlpage-linux-test-binaries path: target - name: Extract Linux Rust test binaries run: | mkdir -p target/sqlpage-test-binaries tar -xzf target/sqlpage-linux-test-binaries.tar.gz -C target/sqlpage-test-binaries - name: Install PostgreSQL ODBC driver if: matrix.setup_odbc run: sudo apt-get install -y odbc-postgresql - name: Install Oracle ODBC driver if: matrix.database == 'oracle' run: | sudo apt-get install -y alien libaio1t64 libodbcinst2 unixodbc sudo rpm --import https://yum.oracle.com/RPM-GPG-KEY-oracle-ol8 wget https://yum.oracle.com/repo/OracleLinux/OL8/oracle/instantclient21/x86_64/getPackage/oracle-instantclient-{basic,odbc}-21.21.0.0.0-1.el8.x86_64.rpm sudo alien -i oracle-instantclient-basic-21.21.0.0.0-1.el8.x86_64.rpm sudo alien -i oracle-instantclient-odbc-21.21.0.0.0-1.el8.x86_64.rpm sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1 sudo /usr/lib/oracle/21/client64/bin/odbc_update_ini.sh / /usr/lib/oracle/21/client64/lib echo "LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV" - name: Start database container if: matrix.container != '' run: docker compose up --wait ${{ matrix.container }} - name: Show container logs if: failure() && matrix.container != '' run: docker compose logs ${{ matrix.container }} - name: Run tests against ${{ matrix.database }} timeout-minutes: 5 run: | set -euo pipefail shopt -s nullglob test_binaries=(target/sqlpage-test-binaries/*) if ((${#test_binaries[@]} == 0)); then echo "No test binaries were found in target/sqlpage-test-binaries" >&2 exit 1 fi for test_binary in "${test_binaries[@]}"; do echo "::group::$(basename "$test_binary")" "$test_binary" echo "::endgroup::" done env: DATABASE_URL: ${{ matrix.db_url }} MALLOC_CHECK_: 3 MALLOC_PERTURB_: 10 windows_test: runs-on: windows-latest steps: - uses: actions/checkout@v6 - name: Set up cargo cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 env: NODE_OPTIONS: --no-deprecation - name: Set up Windows incremental cache uses: actions/cache@v5 with: path: | target/debug/.fingerprint/sqlpage-* target/debug/build/sqlpage-* target/debug/deps/libsqlpage-*.rlib target/debug/deps/libsqlpage-*.rmeta target/debug/deps/sqlpage-*.d target/debug/incremental/sqlpage-* key: windows-incremental-${{ github.event.pull_request.number || github.ref_name }}-${{ github.sha }} restore-keys: | windows-incremental-${{ github.event.pull_request.number || github.ref_name }}- - run: cargo test env: CARGO_INCREMENTAL: 1 RUST_BACKTRACE: 1 - name: Upload Windows binary uses: actions/upload-artifact@v7 with: name: sqlpage-windows-debug path: "target/debug/sqlpage.exe" playwright: runs-on: ubuntu-latest timeout-minutes: 10 needs: compile_and_lint defaults: run: working-directory: ./tests/end-to-end steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: lts/* cache: 'npm' cache-dependency-path: ./tests/end-to-end/package-lock.json - run: sudo apt-get update && sudo apt-get install -y unixodbc-dev - run: npm ci && npx playwright install --with-deps chromium # The browser tests exercise the official site, but they do not need a # separate Rust build. Reuse the binary compiled and tested above. - name: Download Linux binary uses: actions/download-artifact@v8 with: name: sqlpage-linux-debug path: ${{ runner.temp }}/sqlpage-bin - name: Start official site and wait for it to be ready timeout-minutes: 1 run: | chmod +x "${{ runner.temp }}/sqlpage-bin/sqlpage" "${{ runner.temp }}/sqlpage-bin/sqlpage" 2>/tmp/stderrlog & tail -f /tmp/stderrlog | grep -q "started successfully" working-directory: ./examples/official-site - name: Run Playwright tests run: npx playwright test - name: show server logs if: failure() run: cat /tmp/stderrlog - uses: actions/upload-artifact@v7 if: always() with: name: playwright-report path: ./tests/end-to-end/playwright-report/ retention-days: 30 docker_build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - platform: linux/arm/v7 variant: minimal tag_suffix: -linux-arm-v7 - platform: linux/arm64 variant: minimal tag_suffix: -linux-arm64 - platform: linux/amd64 variant: duckdb tag_suffix: -linux-amd64-duckdb - platform: linux/arm64 variant: duckdb tag_suffix: -linux-arm64-duckdb steps: - name: Checkout uses: actions/checkout@v6 - id: cache-scope name: Docker cache scope run: | ref_scope="main" if [[ "${{ github.event_name }}" == "pull_request" ]]; then ref_scope="pr-${{ github.event.pull_request.number }}" fi recipe_hash="${{ hashFiles('Dockerfile', '.cargo/**', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'scripts/**', 'sqlpage/**') }}" { echo "current=sqlpage-${ref_scope}${{ matrix.tag_suffix }}-${recipe_hash}" echo "main=sqlpage-main${{ matrix.tag_suffix }}-${recipe_hash}" } >> "$GITHUB_OUTPUT" - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY_IMAGE }} flavor: suffix=${{ matrix.tag_suffix }} labels: | org.opencontainers.image.created=1970-01-01T00:00:00Z - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub if: github.event_name != 'pull_request' uses: docker/login-action@v4 with: username: ${{ env.REGISTRY_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@v7 with: # Use BuildKit's Git context instead of the mutable runner workspace. # The dependency cache should be keyed by committed source, not by a # per-job local context stream. context: "{{defaultContext}}" platforms: ${{ matrix.platform }} target: ${{ matrix.variant }} labels: ${{ steps.meta.outputs.labels }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} cache-from: | type=gha,scope=${{ steps.cache-scope.outputs.current }} type=gha,scope=${{ steps.cache-scope.outputs.main }} type=registry,ref=${{ env.REGISTRY_IMAGE }}:main${{ matrix.tag_suffix }} cache-to: type=gha,scope=${{ steps.cache-scope.outputs.current }},mode=max - name: Export digest if: github.event_name != 'pull_request' run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v7 if: github.event_name != 'pull_request' with: name: digests-${{ matrix.variant }}${{ matrix.tag_suffix }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 docker_build_amd64_minimal: name: docker_build (linux/amd64, minimal) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY_IMAGE }} flavor: suffix=-linux-amd64 labels: | org.opencontainers.image.created=1970-01-01T00:00:00Z - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - id: cache-scope name: Docker cache scope run: | ref_scope="main" if [[ "${{ github.event_name }}" == "pull_request" ]]; then ref_scope="pr-${{ github.event.pull_request.number }}" fi recipe_hash="${{ hashFiles('Dockerfile', '.cargo/**', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'scripts/**', 'sqlpage/**') }}" { echo "artifact=sqlpage-${ref_scope}-linux-amd64-hurl-${recipe_hash}" echo "current=sqlpage-${ref_scope}-linux-amd64-${recipe_hash}" echo "main=sqlpage-main-linux-amd64-${recipe_hash}" } >> "$GITHUB_OUTPUT" - name: Login to Docker Hub if: github.event_name != 'pull_request' uses: docker/login-action@v4 with: username: ${{ env.REGISTRY_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build image for Hurl examples uses: docker/build-push-action@v7 with: # Use BuildKit's Git context instead of the mutable runner workspace. # The dependency cache should be keyed by committed source, not by a # per-job local context stream. context: "{{defaultContext}}" platforms: linux/amd64 target: minimal labels: ${{ steps.meta.outputs.labels }} tags: | ${{ steps.meta.outputs.tags }} ${{ env.REGISTRY_IMAGE }}:main outputs: type=docker,dest=${{ runner.temp }}/sqlpage.tar cache-from: | type=gha,scope=${{ steps.cache-scope.outputs.artifact }} type=gha,scope=${{ steps.cache-scope.outputs.current }} type=gha,scope=${{ steps.cache-scope.outputs.main }} type=registry,ref=${{ env.REGISTRY_IMAGE }}:main-linux-amd64 cache-to: type=gha,scope=${{ steps.cache-scope.outputs.artifact }},mode=max - name: Upload SQLPage image uses: actions/upload-artifact@v7 with: name: sqlpage-linux-amd64-minimal-image path: ${{ runner.temp }}/sqlpage.tar if-no-files-found: error retention-days: 1 - name: Build and push by digest id: build if: github.event_name != 'pull_request' uses: docker/build-push-action@v7 with: # Use BuildKit's Git context instead of the mutable runner workspace. # The dependency cache should be keyed by committed source, not by a # per-job local context stream. context: "{{defaultContext}}" platforms: linux/amd64 target: minimal labels: ${{ steps.meta.outputs.labels }} push: true tags: ${{ steps.meta.outputs.tags }} cache-from: | type=gha,scope=${{ steps.cache-scope.outputs.current }} type=gha,scope=${{ steps.cache-scope.outputs.main }} type=registry,ref=${{ env.REGISTRY_IMAGE }}:main-linux-amd64 cache-to: type=gha,scope=${{ steps.cache-scope.outputs.current }},mode=max - name: Export digest if: github.event_name != 'pull_request' run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v7 if: github.event_name != 'pull_request' with: name: digests-minimal-linux-amd64 path: /tmp/digests/* if-no-files-found: error retention-days: 1 hurl_examples: runs-on: ubuntu-latest outputs: examples: ${{ steps.examples.outputs.examples }} steps: - uses: actions/checkout@v6 - id: examples run: | examples="$(find examples -mindepth 2 -maxdepth 2 -name test.hurl -print | sed 's#/test.hurl$##' | sort | jq -R -s -c 'split("\n")[:-1]')" echo "examples=$examples" >> "$GITHUB_OUTPUT" - name: Restore Hurl archive id: hurl-cache uses: actions/cache@v5 with: path: .cache/hurl key: hurl-${{ runner.os }}-x86_64-unknown-linux-gnu-${{ env.HURL_VERSION }} - name: Download Hurl archive if: steps.hurl-cache.outputs.cache-hit != 'true' run: | set -euo pipefail archive="hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu.tar.gz" base_url="https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}" mkdir -p .cache/hurl curl --fail --location --retry 5 --retry-delay 2 --output ".cache/hurl/${archive}" "${base_url}/${archive}" curl --fail --location --retry 5 --retry-delay 2 --output ".cache/hurl/${archive}.sha256" "${base_url}/${archive}.sha256" ( cd .cache/hurl expected_sha="$(tr -d '[:space:]' < "${archive}.sha256")" actual_sha="$(sha256sum "$archive" | cut -d ' ' -f 1)" test "$expected_sha" = "$actual_sha" ) - name: Upload Hurl archive uses: actions/upload-artifact@v7 with: name: hurl-${{ env.HURL_VERSION }}-x86_64-unknown-linux-gnu path: .cache/hurl/* if-no-files-found: error retention-days: 1 hurl: name: hurl (${{ matrix.example }}) runs-on: ubuntu-latest timeout-minutes: 15 needs: - docker_build_amd64_minimal - hurl_examples strategy: fail-fast: false matrix: example: ${{ fromJSON(needs.hurl_examples.outputs.examples) }} steps: - uses: actions/checkout@v6 - name: Download Hurl archive uses: actions/download-artifact@v8 with: name: hurl-${{ env.HURL_VERSION }}-x86_64-unknown-linux-gnu path: ${{ runner.temp }}/hurl - name: Install Hurl run: | set -euo pipefail archive="hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu.tar.gz" ( cd "$RUNNER_TEMP/hurl" expected_sha="$(tr -d '[:space:]' < "${archive}.sha256")" actual_sha="$(sha256sum "$archive" | cut -d ' ' -f 1)" test "$expected_sha" = "$actual_sha" tar xzf "$archive" -C "$RUNNER_TEMP/hurl" ) echo "$RUNNER_TEMP/hurl/hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu/bin" >> "$GITHUB_PATH" - name: Download SQLPage image uses: actions/download-artifact@v8 with: name: sqlpage-linux-amd64-minimal-image path: ${{ runner.temp }}/sqlpage-image - name: Load SQLPage image run: | docker load --input "${{ runner.temp }}/sqlpage-image/sqlpage.tar" docker tag "${{ env.REGISTRY_IMAGE }}:main" "${{ env.REGISTRY_IMAGE }}:latest" - name: Run example Hurl test run: scripts/test-examples-hurl.sh "${{ matrix.example }}" docker_push: runs-on: ubuntu-latest if: github.event_name != 'pull_request' needs: - docker_build_amd64_minimal - docker_build strategy: matrix: variant: - minimal - duckdb steps: - name: Download digests uses: actions/download-artifact@v8 env: NODE_OPTIONS: --no-deprecation with: pattern: digests-${{ matrix.variant }}* merge-multiple: true path: /tmp/digests - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub uses: docker/login-action@v4 with: username: ${{ env.REGISTRY_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create manifest list and push working-directory: /tmp/digests env: TAG_SUFFIX: ${{ matrix.variant != 'minimal' && format('-{0}', matrix.variant) || '' }} run: | tags=(-t "${REGISTRY_IMAGE}:${GITHUB_REF_NAME}${TAG_SUFFIX}") if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then tags+=(-t "${REGISTRY_IMAGE}:latest${TAG_SUFFIX}") fi shopt -s nullglob sources=() for digest in *; do sources+=("${REGISTRY_IMAGE}@sha256:${digest}") done ((${#sources[@]} > 0)) docker buildx imagetools create "${tags[@]}" "${sources[@]}" - name: Inspect image env: TAG_SUFFIX: ${{ matrix.variant != 'minimal' && format('-{0}', matrix.variant) || '' }} run: | docker buildx imagetools inspect "${REGISTRY_IMAGE}:${GITHUB_REF_NAME}${TAG_SUFFIX}"