diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f672779..a92f871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: tags: ['v*'] pull_request: branches: [main] + # GIT-83: run the venue gate by hand (Actions → CI → Run workflow) before + # tagging. Publishing still happens only on a v* tag. + workflow_dispatch: jobs: build: @@ -51,8 +54,117 @@ jobs: - name: Dependency audit (no unused deps) run: npx depcheck --ignores="@types/*" --json | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const unused=d.dependencies||[]; if(unused.length){console.error('Unused dependencies:',unused);process.exit(1)}" + # GIT-83 / GIT-112: the hook suite on a runner WITHOUT jq (or ps). The hooks + # fall back to node and bash builtins there; on 1.10.0 that fallback left + # Stop enforcement inactive and nothing noticed, because every runner and + # dev machine the suite had run on had jq. + hooks-without-jq: + runs-on: ubuntu-latest + container: node:20-bookworm-slim + steps: + - uses: actions/checkout@v4 + + - name: Runner has no jq and no ps + run: | + if command -v jq >/dev/null; then echo "::error::jq is installed; this job must run without it"; exit 1; fi + if command -v ps >/dev/null; then echo "::error::ps is installed; this job must run without it"; exit 1; fi + bash --version | head -1; node --version + + - name: Hook tests + run: bash hooks/tests/test-hooks.sh + + # GIT-83: the release gate. Customers never run SQL after setup (standing + # rule, 2026-09-21), so a release must work on a store still on the v1.8.0 + # setup.sql — the venue is put on exactly that schema first, then the + # current one. Runs on v* tags (publish depends on it) and on demand. + # + # Needs repository secrets, all for the DISPOSABLE venue (never production; + # scripts/venue-schema.sh and the driver both refuse it): + # VENUE_REF project ref + # VENUE_SUPABASE_URL https://.supabase.co + # VENUE_SERVICE_ROLE_KEY service role key + # VENUE_DB_URL postgres:// URL (session pooler) for schema resets + venue-gate: + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 30 + # One venue: two runs would wipe each other's data. + concurrency: + group: gitmem-venue + cancel-in-progress: false + env: + VENUE_REF: ${{ secrets.VENUE_REF }} + VENUE_DB_URL: ${{ secrets.VENUE_DB_URL }} + VENUE_SUPABASE_URL: ${{ secrets.VENUE_SUPABASE_URL }} + VENUE_SERVICE_ROLE_KEY: ${{ secrets.VENUE_SERVICE_ROLE_KEY }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # the v1.8.0 tag supplies the floor schema + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci --legacy-peer-deps + - run: npm run build + + # Missing secrets fail the gate — a skipped gate would let publish through. + - name: Venue configured + run: | + missing="" + for v in VENUE_REF VENUE_DB_URL VENUE_SUPABASE_URL VENUE_SERVICE_ROLE_KEY; do + [ -n "${!v}" ] || missing="$missing $v" + done + if [ -n "$missing" ]; then echo "::error::venue gate cannot run, missing secrets:$missing"; exit 1; fi + umask 077 + printf 'SUPABASE_URL=%s\nSUPABASE_SERVICE_ROLE_KEY=%s\nVENUE_REF=%s\n' \ + "$VENUE_SUPABASE_URL" "$VENUE_SERVICE_ROLE_KEY" "$VENUE_REF" > "$RUNNER_TEMP/venue.env" + echo "VENUE_ENV=$RUNNER_TEMP/venue.env" >> "$GITHUB_ENV" + echo "RUNS=$RUNNER_TEMP/venue-runs" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/venue-runs" + git show v1.8.0:schema/setup.sql > "$RUNNER_TEMP/setup-v1.8.0.sql" + + - name: Venue on the v1.8.0 setup.sql + run: scripts/venue-schema.sh reset-to "$RUNNER_TEMP/setup-v1.8.0.sql" + + - name: Driver on v1.8.0 — flow + run: node tests/e2e/blank-supabase.mjs flow --label v180-flow --out "$RUNS" + + - name: Driver on v1.8.0 — egress + run: node tests/e2e/blank-supabase.mjs egress --label v180-egress --out "$RUNS" --rows 250 --usage 5 + + - name: Driver on v1.8.0 — projects (when the driver has it) + run: | + # The usage line lists the modes; a bare "projects" string also appears in a path. + if grep -q 'flow|egress|projects' tests/e2e/blank-supabase.mjs; then + node tests/e2e/blank-supabase.mjs projects --label v180-projects --out "$RUNS" + else + echo "driver has no projects mode in this revision" + fi + + # The current setup.sql must apply on top of v1.8.0, and apply again. + - name: Venue on the current setup.sql (applied twice) + run: | + scripts/venue-schema.sh apply schema/setup.sql + scripts/venue-schema.sh apply schema/setup.sql + + - name: Driver on the current schema — flow + run: node tests/e2e/blank-supabase.mjs flow --label current-flow --out "$RUNS" + + - name: Driver reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: venue-gate-${{ github.run_id }} + # netlogs record method, path, status and body size only — no bodies or headers. + path: ${{ runner.temp }}/venue-runs + if-no-files-found: ignore + retention-days: 14 + publish: - needs: build + needs: [build, hooks-without-jq, venue-gate] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') permissions: diff --git a/scripts/venue-schema.sh b/scripts/venue-schema.sh new file mode 100755 index 0000000..9e35ca5 --- /dev/null +++ b/scripts/venue-schema.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# GIT-83: put the disposable venue on a given schema, for the release gate. +# +# scripts/venue-schema.sh reset-to drop gitmem objects, then apply +# scripts/venue-schema.sh apply apply on top (re-applicability) +# add --dry-run to print what would run and exit +# +# Needs VENUE_REF and VENUE_DB_URL (a postgres:// URL for the venue only). +# Refuses production, and any URL that does not name VENUE_REF, before +# connecting. Uses psql with ON_ERROR_STOP, in one transaction per call. +set -euo pipefail + +DENY_REFS="cjptxyezuxdiinufgrrm" # production GitMem — never touch +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RESET_SQL="$HERE/../tests/e2e/venue/reset-gitmem-objects.sql" + +die() { echo "[venue-schema] $*" >&2; exit 2; } + +mode="${1:-}"; sql="${2:-}"; dry=false +[ "${3:-}" = "--dry-run" ] && dry=true +case "$mode" in reset-to|apply) ;; *) die "usage: venue-schema.sh reset-to|apply [--dry-run]" ;; esac +[ -n "$sql" ] && [ -f "$sql" ] || die "schema file not found: ${sql:-}" +[ -n "${VENUE_REF:-}" ] || die "VENUE_REF is not set" +[ -n "${VENUE_DB_URL:-}" ] || die "VENUE_DB_URL is not set" +for d in $DENY_REFS; do + [ "$VENUE_REF" != "$d" ] || die "refusing: VENUE_REF is production ($d)" + case "$VENUE_DB_URL" in *"$d"*) die "refusing: VENUE_DB_URL names production ($d)" ;; esac +done +case "$VENUE_DB_URL" in + postgres://*|postgresql://*) ;; + *) die "refusing: VENUE_DB_URL is not a postgres:// URL" ;; +esac +case "$VENUE_DB_URL" in *"$VENUE_REF"*) ;; *) die "refusing: VENUE_DB_URL does not name the venue ($VENUE_REF)" ;; esac + +files=() +[ "$mode" = "reset-to" ] && files+=("$RESET_SQL") +files+=("$sql") +if $dry; then + echo "[venue-schema] would apply to venue $VENUE_REF, one transaction: ${files[*]}" + exit 0 +fi +command -v psql >/dev/null || die "psql not found" +args=() +for f in "${files[@]}"; do args+=(-f "$f"); done +# NOTIFY is delivered at commit: PostgREST reloads its schema cache before the driver runs. +PGCONNECT_TIMEOUT=20 psql "$VENUE_DB_URL" -X -q -v ON_ERROR_STOP=1 --single-transaction "${args[@]}" \ + -c "NOTIFY pgrst, 'reload schema'" +echo "[venue-schema] venue $VENUE_REF: $mode $(basename "$sql") OK" diff --git a/tests/e2e/venue/reset-gitmem-objects.sql b/tests/e2e/venue/reset-gitmem-objects.sql new file mode 100644 index 0000000..b1aed51 --- /dev/null +++ b/tests/e2e/venue/reset-gitmem-objects.sql @@ -0,0 +1,21 @@ +-- GIT-83: return the DISPOSABLE venue to a blank slate so a setup.sql lands +-- exactly as it would on a new customer project. Drops only the objects +-- schema/setup.sql creates. Run only through scripts/venue-schema.sh, which +-- refuses production and any database that is not the configured venue. +DROP TABLE IF EXISTS + gitmem_license_activations, gitmem_licenses, + gitmem_scar_usage, gitmem_decisions, gitmem_threads, knowledge_triples, + gitmem_query_metrics, scar_enforcement_variants, + gitmem_learnings, gitmem_sessions + CASCADE; -- also removes the *_lite views, triggers and policies +DO $$ +DECLARE r record; +BEGIN + FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'public' AND p.proname IN + ('gitmem_semantic_search','gitmem_scar_search','refresh_scar_behavioral_scores', + 'gitmem_update_timestamp','gitmem_validate_license','gitmem_deactivate_device') + LOOP EXECUTE 'DROP FUNCTION ' || r.sig || ' CASCADE'; END LOOP; +END $$; +-- PostgREST must see the new shape before the driver's first request. +NOTIFY pgrst, 'reload schema';