From b7bb8ecc5db9788a5eb1f7766dfef397a2f1641f Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Fri, 16 Oct 2026 09:49:28 +0800 Subject: [PATCH 1/3] ci: verify reusable PostgreSQL rustls SQLx CLI binaries --- .github/actions/sqlx-cli-tool/action.yml | 39 +++++++ .github/scripts/select-rust-ci-scope.sh | 7 ++ .github/scripts/sqlx-cli-tool.sh | 80 +++++++++++++ .github/scripts/test-select-rust-ci-scope.sh | 4 +- .github/scripts/test-sqlx-cli-tool.sh | 53 +++++++++ .github/scripts/verify-sqlx-cli-postgres.sh | 113 +++++++++++++++++++ .github/workflows/ploy-ci.yml | 53 ++++++++- 7 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 .github/actions/sqlx-cli-tool/action.yml create mode 100644 .github/scripts/sqlx-cli-tool.sh create mode 100644 .github/scripts/test-sqlx-cli-tool.sh create mode 100644 .github/scripts/verify-sqlx-cli-postgres.sh diff --git a/.github/actions/sqlx-cli-tool/action.yml b/.github/actions/sqlx-cli-tool/action.yml new file mode 100644 index 000000000..abbfd2912 --- /dev/null +++ b/.github/actions/sqlx-cli-tool/action.yml @@ -0,0 +1,39 @@ +name: PostgreSQL rustls SQLx CLI +description: Restore or build the pinned PostgreSQL-only rustls CLI profile +inputs: + probe: + description: Use an isolated branch diagnostic cache on explicit manual runs + default: 'false' +outputs: + cache-hit: + description: Exact executable cache hit + value: ${{ steps.restore.outputs.cache-hit }} + key: + description: Tool/compiler/platform cache identity + value: ${{ steps.prepare.outputs.key }} +runs: + using: composite + steps: + - name: Prepare SQLx CLI cache + id: prepare + shell: bash + env: + SQLX_CACHE_PROBE: ${{ inputs.probe }} + run: bash "$GITHUB_WORKSPACE/.github/scripts/sqlx-cli-tool.sh" prepare + - name: Restore SQLx CLI + id: restore + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + key: ${{ steps.prepare.outputs.key }} + path: ${{ steps.prepare.outputs.path }} + - name: Install or verify SQLx CLI + shell: bash + env: + CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} + run: bash "$GITHUB_WORKSPACE/.github/scripts/sqlx-cli-tool.sh" install "$CACHE_HIT" + - name: Save SQLx CLI + if: steps.restore.outputs.cache-hit != 'true' && steps.prepare.outputs.save_allowed == 'true' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + key: ${{ steps.prepare.outputs.key }} + path: ${{ steps.prepare.outputs.path }} diff --git a/.github/scripts/select-rust-ci-scope.sh b/.github/scripts/select-rust-ci-scope.sh index 85da24fad..8e3745621 100755 --- a/.github/scripts/select-rust-ci-scope.sh +++ b/.github/scripts/select-rust-ci-scope.sh @@ -761,6 +761,13 @@ for path in "${paths[@]}"; do [[ $event == pull_request ]] && select_job ploy/commit-hygiene continue ;; + .github/actions/sqlx-cli-tool/*|.github/scripts/sqlx-cli-tool.sh|.github/scripts/test-sqlx-cli-tool.sh|.github/scripts/verify-sqlx-cli-postgres.sh) + select_job ci/ci-contracts + select_job ploy/workflow-lint + select_job ploy/sqlx-cli-equivalence + [[ $event == pull_request ]] && select_job ploy/commit-hygiene + continue + ;; .github/scripts/cargo-license-tool.sh|.github/scripts/test-cargo-license-tool.sh) select_job ci/ci-contracts select_job ploy/workflow-lint diff --git a/.github/scripts/sqlx-cli-tool.sh b/.github/scripts/sqlx-cli-tool.sh new file mode 100644 index 000000000..1ce7e9dcd --- /dev/null +++ b/.github/scripts/sqlx-cli-tool.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Reuse only the original PostgreSQL/rustls CLI profile, not default-feature binaries. +set -euo pipefail +version=0.8.6 +[[ ${SQLX_CLI_VERSION:-$version} == "$version" ]] || { echo 'SQLx recipe version mismatch' >&2; exit 2; } +tool_root=${RUNNER_TEMP:?}/sqlx-cli-tool +receipt=$tool_root/receipt.json +script=$(cd "$(dirname "$0")" && pwd)/$(basename "$0") +digest() { sha256sum | awk '{print $1}'; } +identity() { + { printf '%s\n' "$version" 'no-default-features;postgres,rustls;locked' \ + "${RUNNER_OS:?}" "${RUNNER_ARCH:?}" "${ImageOS:?}" + rustc -vV + cat "$script" + } | digest +} +case ${1:-} in + prepare) + namespace=trusted + save_allowed=false + if [[ ${GITHUB_REF:-} == refs/heads/main ]]; then + case ${GITHUB_EVENT_NAME:-} in push|schedule|workflow_dispatch) save_allowed=true ;; esac + fi + case ${SQLX_CACHE_PROBE:-false} in + true) + [[ ${GITHUB_EVENT_NAME:-} == workflow_dispatch ]] || exit 2 + if [[ ${GITHUB_REF:-} != refs/heads/main ]]; then + [[ ${GITHUB_SHA:-} =~ ^[a-f0-9]{40}$ ]] || exit 2 + namespace=probe-$GITHUB_SHA + save_allowed=true + fi + ;; + false|'') ;; + *) exit 2 ;; + esac + printf 'key=sqlx-cli-v1-%s-%s\npath=%s\nsave_allowed=%s\n' \ + "$namespace" "$(identity)" "$tool_root" "$save_allowed" >>"${GITHUB_OUTPUT:?}" + ;; + install) + began=$SECONDS + case ${2:-} in + true) ;; + false|'') + toolchain=$(rustup show active-toolchain | awk '{print $1}') + rm -rf -- "$tool_root" + (cd "${RUNNER_TEMP:?}" && \ + env -u CARGO_TARGET_DIR -u CARGO_BUILD_TARGET -u RUSTFLAGS -u CARGO_ENCODED_RUSTFLAGS \ + RUSTUP_TOOLCHAIN="$toolchain" cargo install sqlx-cli --locked --version "$version" \ + --no-default-features --features rustls,postgres --root "$tool_root") + # Cargo records the actual feature selection, not just the package version. + jq -e --arg version "$version" ' + .installs | to_entries | map(select(.key | startswith("sqlx-cli "+$version+" "))) | + length==1 and (.[0].value | .no_default_features==true and .all_features==false and + (.features|sort)==["postgres","rustls"] and (.bins|sort)==["cargo-sqlx","sqlx"]) + ' "$tool_root/.crates2.json" >/dev/null + jq -n --arg version "$version" --arg identity "$(identity)" \ + --arg sqlx "$(digest <"$tool_root/bin/sqlx")" \ + --arg cargo_sqlx "$(digest <"$tool_root/bin/cargo-sqlx")" \ + '{schema:"monday.sqlx-cli-cache.v1",version:$version,features:["postgres","rustls"], + no_default_features:true,identity:$identity,sqlx:$sqlx,cargo_sqlx:$cargo_sqlx}' >"$receipt" + rm -f "$tool_root/.crates.toml" "$tool_root/.crates2.json" + ;; + *) exit 2 ;; + esac + for name in sqlx cargo-sqlx; do + [[ -f $tool_root/bin/$name && -x $tool_root/bin/$name && ! -L $tool_root/bin/$name ]] + done + [[ -f $receipt && ! -L $receipt ]] + jq -e --arg version "$version" --arg identity "$(identity)" \ + --arg sqlx "$(digest <"$tool_root/bin/sqlx")" --arg cargo_sqlx "$(digest <"$tool_root/bin/cargo-sqlx")" ' + .schema=="monday.sqlx-cli-cache.v1" and .version==$version and .identity==$identity and + .features==["postgres","rustls"] and .no_default_features==true and .sqlx==$sqlx and .cargo_sqlx==$cargo_sqlx + ' "$receipt" >/dev/null + [[ $("$tool_root/bin/sqlx" --version) == "sqlx-cli $version" ]] + printf '%s\n' "$tool_root/bin" >>"${GITHUB_PATH:?}" + printf 'sqlx-cli version=%s features=postgres,rustls cache_hit=%s install_seconds=%s sha256=%s\n' \ + "$version" "${2:-false}" "$((SECONDS-began))" "$(digest <"$tool_root/bin/sqlx")" + ;; + *) echo 'usage: sqlx-cli-tool.sh prepare|install [cache-hit]' >&2; exit 2 ;; +esac diff --git a/.github/scripts/test-select-rust-ci-scope.sh b/.github/scripts/test-select-rust-ci-scope.sh index e1065a073..c75322dec 100755 --- a/.github/scripts/test-select-rust-ci-scope.sh +++ b/.github/scripts/test-select-rust-ci-scope.sh @@ -1347,7 +1347,7 @@ if grep -Fq 'test-polymarket-market-recorder-release.sh' <<<"$rust_job_block"; t grep -Fq 'ci-owner-cache.sh" cleanup' <<<"$rust_job_block" ploy_workflow="$script_dir/../workflows/ploy-ci.yml" -grep -Fqx " group: prediction-markets-\${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'research-cache-probe-' || '' }}\${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}" "$ploy_workflow" +grep -Fqx " group: prediction-markets-\${{ github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe && 'sqlx-cli-probe-' || '' }}\${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'research-cache-probe-' || '' }}\${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}" "$ploy_workflow" grep -Fqx " cancel-in-progress: \${{ github.ref != 'refs/heads/main' }}" "$ploy_workflow" [[ $(grep -Fxc ' branches: [main, develop]' "$ploy_workflow") -eq 2 ]] if sed -n '/^ push:$/,/^ workflow_dispatch:$/p' "$ploy_workflow" \ @@ -1574,7 +1574,7 @@ grep -Fqx ' - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea186 grep -Fqx ' EXPECTED_JOBS: ${{ needs.scope.outputs.jobs }}' "$ci_workflow" grep -Fqx " run: printf '%s' \"\$GATE_NEEDS\" | bash .github/scripts/verify-ci-gate.sh --job-prefix ci --expected-jobs \"\$EXPECTED_JOBS\"" "$ci_workflow" grep -Fqx ' prediction-markets-gate:' "$ploy_workflow" -grep -Fqx " name: \${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }}" "$ploy_workflow" +grep -Fqx " name: \${{ github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe && 'SQLx CLI diagnostic gate' || github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }}" "$ploy_workflow" grep -Fqx ' - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0' "$ploy_workflow" grep -Fqx ' EXPECTED_JOBS: ${{ needs.research-image-scope.outputs.jobs }}' "$ploy_workflow" grep -Fqx " run: printf '%s' \"\$GATE_NEEDS\" | bash .github/scripts/verify-ci-gate.sh --job-prefix ploy --expected-jobs \"\$EXPECTED_JOBS\"" "$ploy_workflow" diff --git a/.github/scripts/test-sqlx-cli-tool.sh b/.github/scripts/test-sqlx-cli-tool.sh new file mode 100644 index 000000000..6f59e7477 --- /dev/null +++ b/.github/scripts/test-sqlx-cli-tool.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/bin" "$work/runner" +export RUNNER_TEMP="$work/runner" RUNNER_OS=Linux RUNNER_ARCH=X64 ImageOS=ubuntu24 +export GITHUB_OUTPUT="$work/output" GITHUB_PATH="$work/path" GITHUB_EVENT_NAME=push +export GITHUB_REF=refs/heads/main GITHUB_SHA=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +export INSTALL_CALLS="$work/installs" +helper=$root/.github/scripts/sqlx-cli-tool.sh +cat >"$work/bin/rustc" <<'SH' +#!/usr/bin/env bash +printf 'rustc %s\n' "${FIXTURE_COMPILER:-1.98.1}" +SH +printf '#!/usr/bin/env bash\necho "1.98.1-x86_64-unknown-linux-gnu (default)"\n' >"$work/bin/rustup" +cat >"$work/bin/cargo" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +[[ $* == 'install sqlx-cli --locked --version 0.8.6 --no-default-features --features rustls,postgres --root '* ]] +printf 'install\n' >>"$INSTALL_CALLS" +dest=${!#} +mkdir -p "$dest/bin" +for bin in sqlx cargo-sqlx; do + printf '#!/usr/bin/env bash\necho "sqlx-cli 0.8.6"\n' >"$dest/bin/$bin" + chmod +x "$dest/bin/$bin" +done +jq -n --argjson defaults "${BAD_DEFAULTS:-false}" \ + '{installs:{"sqlx-cli 0.8.6 (registry+fixture)":{features:["postgres","rustls"],all_features:false, + no_default_features:($defaults|not),bins:["sqlx","cargo-sqlx"]}}}' >"$dest/.crates2.json" +SH +chmod +x "$work/bin/"* +export PATH="$work/bin:$PATH" +prepare() { : >"$GITHUB_OUTPUT"; bash "$helper" prepare; } +key() { sed -n 's/^key=//p' "$GITHUB_OUTPUT"; } +prepare; normal=$(key); grep -qx save_allowed=true "$GITHUB_OUTPUT" +GITHUB_REF=refs/pull/1/merge GITHUB_EVENT_NAME=pull_request prepare +[[ $(key) == "$normal" ]]; grep -qx save_allowed=false "$GITHUB_OUTPUT" +SQLX_CACHE_PROBE=true GITHUB_EVENT_NAME=workflow_dispatch GITHUB_REF=refs/heads/probe prepare +[[ $(key) == sqlx-cli-v1-probe-* ]]; grep -qx save_allowed=true "$GITHUB_OUTPUT" +SQLX_CACHE_PROBE=true GITHUB_EVENT_NAME=workflow_dispatch prepare; [[ $(key) == "$normal" ]] +ImageOS=ubuntu26 prepare; [[ $(key) != "$normal" ]] +FIXTURE_COMPILER=1.99.0 prepare; [[ $(key) != "$normal" ]] +if SQLX_CACHE_PROBE=true prepare; then exit 1; fi +if SQLX_CLI_VERSION=0.9.0 prepare; then exit 1; fi +if BAD_DEFAULTS=true bash "$helper" install false; then echo 'default feature profile accepted' >&2; exit 1; fi +bash "$helper" install false +bash "$helper" install true +[[ $(wc -l <"$INSTALL_CALLS") -eq 2 ]] +printf 'corrupt\n' >>"$RUNNER_TEMP/sqlx-cli-tool/bin/cargo-sqlx" +if bash "$helper" install true; then echo 'corrupt cached cargo-sqlx accepted' >&2; exit 1; fi +[[ $(wc -l <"$INSTALL_CALLS") -eq 2 ]] +printf 'SQLx pinned profile cache contracts: ok\n' diff --git a/.github/scripts/verify-sqlx-cli-postgres.sh b/.github/scripts/verify-sqlx-cli-postgres.sh new file mode 100644 index 000000000..affd4fd44 --- /dev/null +++ b/.github/scripts/verify-sqlx-cli-postgres.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Real ephemeral PostgreSQL/TLS checks belong on GitHub runners, never local PG. +set -euo pipefail +[[ ${GITHUB_ACTIONS:-} == true ]] || { echo 'Run this PostgreSQL contract in GitHub Actions' >&2; exit 2; } +baseline=${1:?baseline sqlx executable} +candidate=${2:?restored sqlx executable} +root=$(cd "$(dirname "$0")/../.." && pwd) +work=$(mktemp -d "${RUNNER_TEMP:?}/sqlx-postgres.XXXXXX") +evidence=${RUNNER_TEMP}/sqlx-cli-evidence +mkdir -p "$evidence" "$work/migrations" +tls=sqlx-tls-${GITHUB_RUN_ID:?}-${GITHUB_RUN_ATTEMPT:?} +plain=sqlx-plain-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT} +cleanup() { + docker rm -fv "$tls" "$plain" >/dev/null 2>&1 || true + rm -rf "$work" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +openssl req -x509 -newkey rsa:2048 -nodes -days 2 -subj /CN=Monday-SQLx-test-CA \ + -keyout "$work/ca.key" -out "$work/ca.crt" >/dev/null 2>&1 +openssl req -newkey rsa:2048 -nodes -subj /CN=localhost \ + -keyout "$work/server.key" -out "$work/server.csr" >/dev/null 2>&1 +printf 'subjectAltName=DNS:localhost\nextendedKeyUsage=serverAuth\n' >"$work/server.ext" +openssl x509 -req -in "$work/server.csr" -CA "$work/ca.crt" -CAkey "$work/ca.key" \ + -CAcreateserial -days 2 -extfile "$work/server.ext" -out "$work/server.crt" >/dev/null 2>&1 +openssl req -x509 -newkey rsa:2048 -nodes -days 2 -subj /CN=Wrong-CA \ + -keyout "$work/wrong.key" -out "$work/wrong.crt" >/dev/null 2>&1 +docker run -d --name "$plain" -p 127.0.0.1::5432 --tmpfs /var/lib/postgresql/data \ + -e POSTGRES_USER=ploy -e POSTGRES_PASSWORD=sqlx-contract postgres:15 >/dev/null +docker run -d --name "$tls" -p 127.0.0.1::5432 --tmpfs /var/lib/postgresql/data \ + -e POSTGRES_USER=ploy -e POSTGRES_PASSWORD=sqlx-contract -v "$work:/certs:ro" \ + --entrypoint bash postgres:15 -ceu ' + cp /certs/server.key /certs/server.crt /tmp/ + chown postgres:postgres /tmp/server.key /tmp/server.crt + chmod 600 /tmp/server.key + exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/tmp/server.crt -c ssl_key_file=/tmp/server.key + ' >/dev/null +for server in "$plain" "$tls"; do + ready=false + for _ in $(seq 1 60); do + if docker exec "$server" pg_isready -h 127.0.0.1 -U ploy >/dev/null 2>&1; then ready=true; break; fi + sleep 1 + done + [[ $ready == true ]] || { docker logs "$server"; exit 1; } +done +plain_port=$(docker port "$plain" 5432/tcp | cut -d: -f2) +tls_port=$(docker port "$tls" 5432/tcp | cut -d: -f2) +cat >"$work/migrations/1_transport.up.sql" <<'SQL' +CREATE TABLE transport_contract(encrypted boolean NOT NULL); +INSERT INTO transport_contract SELECT ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid(); +SQL +printf 'DROP TABLE transport_contract;\n' >"$work/migrations/1_transport.down.sql" + +reject() { + local name=$1; shift + local status=0 + timeout 15 "$@" >"$evidence/$name.log" 2>&1 || status=$? + [[ $status != 0 && $status != 124 ]] || { echo "$name did not reject promptly" >&2; exit 1; } + case "$name" in + *_unknown_ca|*_hostname|*_require_tls) grep -Eiq 'certificate|TLS|SSL' "$evidence/$name.log" ;; + *_sqlite_disabled) grep -Eiq 'driver|sqlite' "$evidence/$name.log" ;; + esac + printf '%s\tpassed\n' "$name" >>"$evidence/checks.tsv" +} +check_tool() { + local label=$1 bin=$2 url db server port expected + [[ $("$bin" --version) == 'sqlx-cli 0.8.6' ]] + for mode in plain tls; do + if [[ $mode == plain ]]; then + server=$plain; port=$plain_port; expected=f + url="postgres://ploy:sqlx-contract@localhost:$port/${label}_${mode}?sslmode=disable" + else + server=$tls; port=$tls_port; expected=t + url="postgres://ploy:sqlx-contract@localhost:$port/${label}_${mode}?sslmode=verify-full&sslrootcert=$work/ca.crt" + fi + db=${label}_${mode} + "$bin" database create --database-url "$url" + "$bin" migrate run --source "$work/migrations" --database-url "$url" + [[ $(docker exec "$server" psql -U ploy -d "$db" -Atc 'SELECT encrypted FROM transport_contract') == "$expected" ]] + "$bin" migrate run --source "$work/migrations" --database-url "$url" + [[ $(docker exec "$server" psql -U ploy -d "$db" -Atc 'SELECT count(*) FROM transport_contract') == 1 ]] + "$bin" migrate revert --source "$work/migrations" --database-url "$url" + [[ -z $(docker exec "$server" psql -U ploy -d "$db" -Atc "SELECT to_regclass('public.transport_contract')") ]] + printf '%s_%s_migrate_idempotent_revert\tpassed\n' "$label" "$mode" >>"$evidence/checks.tsv" + done + reject "${label}_unknown_ca" "$bin" database create --database-url \ + "postgres://ploy:sqlx-contract@localhost:$tls_port/${label}_badca?sslmode=verify-full&sslrootcert=$work/wrong.crt" + reject "${label}_hostname" "$bin" database create --database-url \ + "postgres://ploy:sqlx-contract@127.0.0.1:$tls_port/${label}_badhost?sslmode=verify-full&sslrootcert=$work/ca.crt" + reject "${label}_require_tls" "$bin" database create --database-url \ + "postgres://ploy:sqlx-contract@localhost:$plain_port/${label}_require?sslmode=require" + reject "${label}_sqlite_disabled" "$bin" database create --database-url "sqlite://$work/$label.sqlite" + + # Exercise the repository's complete migration set over authenticated TLS. + db=${label}_application + url="postgres://ploy:sqlx-contract@localhost:$tls_port/$db?sslmode=verify-full&sslrootcert=$work/ca.crt" + "$bin" database create --database-url "$url" + (cd "$root/rust_hft/prediction-markets"; DATABASE_URL="$url" "$bin" migrate run) + docker exec "$tls" psql -U ploy -d "$db" -Atc \ + "SELECT version,description,success,encode(checksum,'hex') FROM _sqlx_migrations ORDER BY version" >"$evidence/$label-migrations.txt" + docker exec "$tls" pg_dump -U ploy -d "$db" --schema-only --no-owner --no-privileges \ + | sed '/^\\restrict /d; /^\\unrestrict /d' >"$evidence/$label-schema.sql" + printf '%s_application_migrations\tpassed\n' "$label" >>"$evidence/checks.tsv" +} +: >"$evidence/checks.tsv" +check_tool baseline "$baseline" +check_tool cached "$candidate" +cmp "$evidence/baseline-migrations.txt" "$evidence/cached-migrations.txt" +cmp "$evidence/baseline-schema.sql" "$evidence/cached-schema.sql" +printf 'PASS: PostgreSQL, TLS acceptance/rejection, migrations, rollback and schema equivalence\n' +cat "$evidence/checks.tsv" diff --git a/.github/workflows/ploy-ci.yml b/.github/workflows/ploy-ci.yml index 804900e29..e01cf311d 100644 --- a/.github/workflows/ploy-ci.yml +++ b/.github/workflows/ploy-ci.yml @@ -11,9 +11,13 @@ on: description: Compare serial and bounded-parallel recipes with the same warmed dependencies type: boolean default: false + sqlx_cli_probe: + description: Compare fresh and cached PostgreSQL/rustls SQLx CLI; not release admission + type: boolean + default: false concurrency: - group: prediction-markets-${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'research-cache-probe-' || '' }}${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }} + group: prediction-markets-${{ github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe && 'sqlx-cli-probe-' || '' }}${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'research-cache-probe-' || '' }}${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: @@ -53,8 +57,19 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} RESEARCH_CACHE_PROBE: ${{ inputs.research_cache_probe }} + SQLX_CLI_PROBE: ${{ inputs.sqlx_cli_probe }} run: | set -euo pipefail + case "${SQLX_CLI_PROBE:-}" in + true) + [[ "$GITHUB_EVENT_NAME" == workflow_dispatch && ${RESEARCH_CACHE_PROBE:-false} != true ]] || exit 2 + printf '%s\n' 'jobs=,ploy/sqlx-cli-equivalence,' 'research_product=none' \ + 'selection_complete=true' >> "$GITHUB_OUTPUT" + exit 0 + ;; + false|'') ;; + *) exit 2 ;; + esac case "${RESEARCH_CACHE_PROBE:-}" in true) [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]] || { @@ -1041,8 +1056,41 @@ jobs: run: bash .github/scripts/run-prediction-platform-smoke.sh + sqlx-cli-equivalence: + name: SQLx CLI PostgreSQL and TLS equivalence + needs: image-smoke-scope + if: ${{ contains(needs.image-smoke-scope.outputs.jobs, ',ploy/sqlx-cli-equivalence,') }} + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: dtolnay/rust-toolchain@89b12181fb390509a0842a86cc55eeb8eb928c1d + with: + toolchain: 1.98.1 + - name: Test SQLx cache contracts + working-directory: . + run: bash .github/scripts/test-sqlx-cli-tool.sh + - name: Compile the original PostgreSQL rustls CLI baseline + run: >- + cargo install sqlx-cli --locked --version "$SQLX_CLI_VERSION" + --no-default-features --features rustls,postgres --root "$RUNNER_TEMP/sqlx-baseline" + - name: Prepare reusable SQLx CLI + uses: ./.github/actions/sqlx-cli-tool + with: + probe: ${{ github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe }} + - name: Compare PostgreSQL and TLS behavior + working-directory: . + run: bash .github/scripts/verify-sqlx-cli-postgres.sh "$RUNNER_TEMP/sqlx-baseline/bin/sqlx" "$RUNNER_TEMP/sqlx-cli-tool/bin/sqlx" + - name: Retain SQLx equivalence evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: sqlx-cli-equivalence + path: ${{ runner.temp }}/sqlx-cli-evidence + if-no-files-found: error + prediction-markets-gate: - name: ${{ github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }} + name: ${{ github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe && 'SQLx CLI diagnostic gate' || github.event_name == 'workflow_dispatch' && inputs.research_cache_probe && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }} needs: - image-smoke-selector - image-smoke-scope @@ -1064,6 +1112,7 @@ jobs: - integration-regressions - strategy-config-contracts - architecture-contracts + - sqlx-cli-equivalence if: ${{ always() }} runs-on: ubuntu-latest steps: From 4b8dde79b2d91bc59eaf9c948e77052daf5f9214 Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Fri, 16 Oct 2026 10:02:45 +0800 Subject: [PATCH 2/3] ci: reuse verified SQLx CLI in all PostgreSQL contract lanes --- .../run-prediction-market-data-contracts.sh | 6 +----- .../run-prediction-research-contracts.sh | 8 ++------ .github/scripts/select-rust-ci-scope.sh | 3 +++ .github/scripts/test-select-rust-ci-scope.sh | 12 ++++++++++-- .github/workflows/ploy-ci.yml | 17 +++++++++++------ 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.github/scripts/run-prediction-market-data-contracts.sh b/.github/scripts/run-prediction-market-data-contracts.sh index 8da66ef6e..9ae71e5cf 100755 --- a/.github/scripts/run-prediction-market-data-contracts.sh +++ b/.github/scripts/run-prediction-market-data-contracts.sh @@ -6,15 +6,11 @@ cd "$(dirname "$0")/../../rust_hft/prediction-markets" : "${DATABASE_URL:?ephemeral PostgreSQL URL required}" : "${PLOY_TEST_DATABASE_URL:?ephemeral PostgreSQL test URL required}" : "${SQLX_CLI_VERSION:?pinned SQLx CLI version required}" +[[ $(sqlx --version) == "sqlx-cli $SQLX_CLI_VERSION" ]] || { echo 'pinned SQLx CLI is not prepared' >&2; exit 2; } cargo check --locked -p ploy-market-data --features live --lib cargo test --locked -p ploy-market-data --features live --lib -cargo install sqlx-cli \ - --locked \ - --version "${SQLX_CLI_VERSION}" \ - --no-default-features \ - --features rustls,postgres sqlx migrate run # Reuse the live lib test above. Missing or renamed tests must fail this lane. diff --git a/.github/scripts/run-prediction-research-contracts.sh b/.github/scripts/run-prediction-research-contracts.sh index 31991e4ad..43f0db903 100755 --- a/.github/scripts/run-prediction-research-contracts.sh +++ b/.github/scripts/run-prediction-research-contracts.sh @@ -3,6 +3,8 @@ set -euo pipefail SECONDS=0 cd "$(dirname "$0")/../../rust_hft/prediction-markets" +: "${SQLX_CLI_VERSION:?pinned SQLx CLI version required}" +[[ $(sqlx --version) == "sqlx-cli $SQLX_CLI_VERSION" ]] || { echo 'pinned SQLx CLI is not prepared' >&2; exit 2; } cargo test --locked -p ploy-feed-loaders --lib cargo test --locked -p ploy-research --lib cargo test --locked -p ploy-research --no-default-features --features rl --lib model::rl @@ -28,12 +30,6 @@ CODEX_CLI_BIN="$handoff_root/mock-codex" CODEX_CLI_TIMEOUT_SECS=10 \ -- --exact --ignored --test-threads=1 --nocapture test -s "$handoff_root/consumer-proof.json" -cargo install sqlx-cli \ - --locked \ - --version "${SQLX_CLI_VERSION}" \ - --no-default-features \ - --features rustls,postgres - sqlx migrate run cargo check --locked -p ploy-research --features db,polars-export,ml,rl,strategy-runtime --lib cargo test --locked -p ploy-research --features db --example persist_research_trace diff --git a/.github/scripts/select-rust-ci-scope.sh b/.github/scripts/select-rust-ci-scope.sh index 8e3745621..142e05578 100755 --- a/.github/scripts/select-rust-ci-scope.sh +++ b/.github/scripts/select-rust-ci-scope.sh @@ -765,6 +765,9 @@ for path in "${paths[@]}"; do select_job ci/ci-contracts select_job ploy/workflow-lint select_job ploy/sqlx-cli-equivalence + select_job ploy/rust-control-plane + select_job ploy/rust-market-data + select_job ploy/rust-research-heavy [[ $event == pull_request ]] && select_job ploy/commit-hygiene continue ;; diff --git a/.github/scripts/test-select-rust-ci-scope.sh b/.github/scripts/test-select-rust-ci-scope.sh index c75322dec..333f17e2a 100755 --- a/.github/scripts/test-select-rust-ci-scope.sh +++ b/.github/scripts/test-select-rust-ci-scope.sh @@ -356,6 +356,10 @@ SH #!/usr/bin/env bash set -euo pipefail printf 'sqlx %s\n' "$*" >>"$CONTRACT_FIXTURE/calls" +if [[ $* == --version ]]; then + if [[ $CONTRACT_CASE == version-failure ]]; then echo 'sqlx-cli 0.9.0'; else echo 'sqlx-cli 0.8.6'; fi + exit 0 +fi [[ $* == 'migrate run' ]] [[ $CONTRACT_CASE != migration-failure ]] || exit 61 printf 'migrated\n' >"$CONTRACT_FIXTURE/migrated" @@ -363,7 +367,7 @@ SH chmod +x "$fixture/bin/cargo" "$fixture/bin/sqlx" local pg_test=predict_fun::tests::postgres_predict_fun_restart_restores_clock_and_persists_failure_readiness local scenario - for scenario in success missing migration-failure test-failure; do + for scenario in success missing migration-failure test-failure version-failure; do rm -f "$fixture/migrated" "$fixture/calls" "$fixture/cwd" result=0 PATH="$fixture/bin:$PATH" CONTRACT_FIXTURE="$fixture" CONTRACT_CASE="$scenario" CONTRACT_PG_TEST="$pg_test" \ @@ -376,15 +380,19 @@ SH [[ $result == 0 ]] [[ $(sort -u "$fixture/cwd") == "$fixture/repo/rust_hft/prediction-markets" ]] cat >"$fixture/expected" < Date: Fri, 16 Oct 2026 10:11:09 +0800 Subject: [PATCH 3/3] test(ci): verify SQLx diagnostic gate isolation --- .github/scripts/test-ci-monitor-scope.sh | 54 +++++++++++++++++++++--- .github/workflows/ploy-ci.yml | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.github/scripts/test-ci-monitor-scope.sh b/.github/scripts/test-ci-monitor-scope.sh index e08b5fdd0..f8713ffe0 100755 --- a/.github/scripts/test-ci-monitor-scope.sh +++ b/.github/scripts/test-ci-monitor-scope.sh @@ -32,7 +32,7 @@ ancestors = lambda do |id| end %w[workflow-lint commit-hygiene rust-format safety-scans audit rust-control-plane rust-runner-lean rust-runner-full rust-market-data rust-research-heavy frontend integration-regressions - strategy-config-contracts architecture-contracts].each do |id| + strategy-config-contracts architecture-contracts sqlx-cli-equivalence].each do |id| abort "#{id} waits for publication history" if ancestors.call(id).include?('research-image-scope') end %w[research-image-binaries research-image-smoke prediction-markets-gate].each do |id| @@ -41,10 +41,13 @@ end abort 'direct selector consults publication history' if prediction.fetch('image-smoke-selector').to_s.include?('select-main-research-scope') abort 'gate ignores carried jobs' unless prediction.fetch('prediction-markets-gate').to_s.include?('needs.research-image-scope.outputs.jobs') diagnostic = "github.event_name == 'workflow_dispatch' && inputs.research_cache_probe" +sqlx_diagnostic = "github.event_name == 'workflow_dispatch' && inputs.sqlx_cli_probe" abort 'probe can replace the required release gate' unless prediction.fetch('prediction-markets-gate').fetch('name') == - "${{ #{diagnostic} && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }}" + "${{ #{sqlx_diagnostic} && 'SQLx CLI diagnostic gate' || #{diagnostic} && 'Research build A/B diagnostic gate' || 'Prediction Markets CI gate' }}" document = YAML.safe_load(File.read("#{repo}/.github/workflows/ploy-ci.yml")) abort 'probe can cancel ordinary CI' unless document.fetch('concurrency').fetch('group').include?("#{diagnostic} && 'research-cache-probe-'") +abort 'SQLx probe can cancel ordinary CI' unless document.fetch('concurrency').fetch('group').include?("#{sqlx_diagnostic} && 'sqlx-cli-probe-'") +abort 'SQLx contract missing from aggregate gate' unless prediction.fetch('prediction-markets-gate').fetch('needs').include?('sqlx-cli-equivalence') File.write("#{work}/diagnostic-plan.sh", prediction.fetch('image-smoke-selector').fetch('steps').find { |step| step['id']=='scope' }.fetch('run')) { 'ci.yml'=>'scope', 'ploy-ci.yml'=>'image-smoke-scope', 'security-enabled.yml'=>'security-scope' @@ -72,7 +75,7 @@ for probe in true false '' invalid; do called="$output.ordinary" code=0 (cd "$probe_repo/rust_hft/prediction-markets" && - RESEARCH_CACHE_PROBE=$probe GITHUB_EVENT_NAME=$event GITHUB_OUTPUT="$output" \ + SQLX_CLI_PROBE=false RESEARCH_CACHE_PROBE=$probe GITHUB_EVENT_NAME=$event GITHUB_OUTPUT="$output" \ ORDINARY_PLAN_CALLED="$called" BASE_SHA=base HEAD_SHA=head bash "$work/diagnostic-plan.sh") \ >"$work/probe.stdout" 2>"$work/probe.stderr" || code=$? if [[ $probe == true && $event == workflow_dispatch ]]; then @@ -89,10 +92,49 @@ for probe in true false '' invalid; do done done +# SQLx diagnostics use the same fail-closed plan boundary without broad research. +for probe in true false '' invalid; do + for event in workflow_dispatch push pull_request; do + output="$work/sqlx-$event-${probe:-absent}.out" + called="$output.ordinary" + code=0 + (cd "$probe_repo/rust_hft/prediction-markets" && + SQLX_CLI_PROBE=$probe RESEARCH_CACHE_PROBE=false GITHUB_EVENT_NAME=$event GITHUB_OUTPUT="$output" \ + ORDINARY_PLAN_CALLED="$called" BASE_SHA=base HEAD_SHA=head bash "$work/diagnostic-plan.sh") \ + >"$work/probe.stdout" 2>"$work/probe.stderr" || code=$? + if [[ $probe == true && $event == workflow_dispatch ]]; then + [[ $code == 0 && ! -e $called ]] + grep -Fqx 'jobs=,ploy/sqlx-cli-equivalence,' "$output" + grep -Fqx research_product=none "$output" + grep -Fqx selection_complete=true "$output" + elif [[ $probe == false || -z $probe ]]; then + [[ $code == 0 ]] + grep -Fqx -- "--event $event --base base --head head" "$called" + else + [[ $code != 0 && ! -e $called && ! -s $output ]] + fi + done +done +for other_probe in true invalid; do + if (cd "$probe_repo/rust_hft/prediction-markets" && + SQLX_CLI_PROBE=true RESEARCH_CACHE_PROBE=$other_probe GITHUB_EVENT_NAME=workflow_dispatch \ + GITHUB_OUTPUT="$work/conflicting.out" ORDINARY_PLAN_CALLED="$work/conflicting.ordinary" \ + bash "$work/diagnostic-plan.sh"); then exit 1; fi + [[ ! -s $work/conflicting.out && ! -e $work/conflicting.ordinary ]] +done + # Diagnostic success cannot satisfy the production publication check reader. -printf '%s\n' '{"check_runs":[{"id":1,"name":"Research build A/B diagnostic gate","status":"completed","conclusion":"success","app":{"slug":"github-actions","id":15368}}]}' >"$work/probe-checks.json" -bash "$script_dir/read-release-required-checks.sh" "$work/probe-checks.json" "$work/probe-release-state" -grep -Fqx prediction_conclusion=missing "$work/probe-release-state" +for name in 'Research build A/B diagnostic gate' 'SQLx CLI diagnostic gate'; do + jq -n --arg name "$name" '{check_runs:[{id:1,name:$name,status:"completed",conclusion:"success",app:{slug:"github-actions",id:15368}}]}' >"$work/probe-checks.json" + bash "$script_dir/read-release-required-checks.sh" "$work/probe-checks.json" "$work/probe-release-state" + grep -Fqx prediction_conclusion=missing "$work/probe-release-state" +done +sqlx_needs='{"image-smoke-selector":{"result":"success"},"image-smoke-scope":{"result":"success"},"research-image-scope":{"result":"success"},"sqlx-cli-equivalence":{"result":"success"}}' +printf '%s' "$sqlx_needs" | bash "$script_dir/verify-ci-gate.sh" --job-prefix ploy --expected-jobs ',ploy/sqlx-cli-equivalence,' +for result in skipped failure cancelled missing; do + payload=$(jq -c --arg result "$result" 'if $result=="missing" then del(."sqlx-cli-equivalence") else ."sqlx-cli-equivalence".result=$result end' <<<"$sqlx_needs") + if printf '%s' "$payload" | bash "$script_dir/verify-ci-gate.sh" --job-prefix ploy --expected-jobs ',ploy/sqlx-cli-equivalence,' >"$work/sqlx-gate.log" 2>&1; then exit 1; fi +done for file in ci.yml ploy-ci.yml security-enabled.yml; do for result in success failure; do diff --git a/.github/workflows/ploy-ci.yml b/.github/workflows/ploy-ci.yml index 69543e81c..a3ac0fa76 100644 --- a/.github/workflows/ploy-ci.yml +++ b/.github/workflows/ploy-ci.yml @@ -62,7 +62,7 @@ jobs: set -euo pipefail case "${SQLX_CLI_PROBE:-}" in true) - [[ "$GITHUB_EVENT_NAME" == workflow_dispatch && ${RESEARCH_CACHE_PROBE:-false} != true ]] || exit 2 + [[ "$GITHUB_EVENT_NAME" == workflow_dispatch && ${RESEARCH_CACHE_PROBE:-false} == false ]] || exit 2 printf '%s\n' 'jobs=,ploy/sqlx-cli-equivalence,' 'research_product=none' \ 'selection_complete=true' >> "$GITHUB_OUTPUT" exit 0