From 949bac591f41e94baa41218643b2ba550eec5bde Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 4 Nov 2026 13:48:24 +0100 Subject: [PATCH 1/3] ci: fix docker image cache, add lint job and test only affected packages --- .github/scripts/ci_matrix.py | 115 +++++++++++++++++++++ .github/workflows/ci.yml | 130 ++++++++++++++++++------ src/realtime/infra/supabase/config.toml | 10 +- src/storage/infra/supabase/config.toml | 2 +- 4 files changed, 218 insertions(+), 39 deletions(-) create mode 100644 .github/scripts/ci_matrix.py diff --git a/.github/scripts/ci_matrix.py b/.github/scripts/ci_matrix.py new file mode 100644 index 000000000..331bd97f0 --- /dev/null +++ b/.github/scripts/ci_matrix.py @@ -0,0 +1,115 @@ +"""Build the CI test matrix from the files a change touches. + +Reads changed file paths from stdin, one per line, or tests every package +when called with --all. Prints GitHub Actions outputs to stdout. +""" + +import fnmatch +import json +import sys + +PYTHON_VERSIONS = ["3.11", "3.12", "3.13", "3.14"] +LOWEST_PYTHON_VERSION = PYTHON_VERSIONS[0] +PACKAGES = [ + "functions", + "realtime", + "storage", + "auth", + "postgrest", + "supabase", + "utils", +] +PACKAGES_WITHOUT_COVERAGE = {"utils"} +IMPORT_NAMES = { + "functions": "supabase_functions", + "realtime": "realtime", + "storage": "storage3", + "auth": "supabase_auth", + "postgrest": "postgrest", + "supabase": "supabase", + "utils": "supabase_utils", +} +DEPENDENTS = { + "utils": set(PACKAGES), + "functions": {"supabase"}, + "realtime": {"supabase"}, + "storage": {"supabase"}, + "auth": {"supabase"}, + "postgrest": {"supabase"}, + "supabase": set(), +} +IGNORED_PATTERNS = [ + "*.md", + ".devcontainer/*", + ".github/*", + "LICENSE", + "sdk-compliance.yaml", + "release-please-config.json", + ".release-please-manifest.json", +] +ALWAYS_TEST_ALL_PATTERNS = [".github/workflows/ci.yml", ".github/scripts/ci_matrix.py"] + + +def affected_packages(paths: list[str]) -> set[str]: + affected: set[str] = set() + for path in paths: + if any(fnmatch.fnmatch(path, pattern) for pattern in ALWAYS_TEST_ALL_PATTERNS): + return set(PACKAGES) + if any(fnmatch.fnmatch(path, pattern) for pattern in IGNORED_PATTERNS): + continue + parts = path.split("/") + if len(parts) > 2 and parts[0] == "src" and parts[1] in DEPENDENTS: + affected.add(parts[1]) + affected |= DEPENDENTS[parts[1]] + else: + return set(PACKAGES) + return affected + + +def flag_name(entry: dict[str, str]) -> str: + return f"{entry['package']}-py{entry['python-version']}-{entry['resolution']}" + + +def matrix_entry(version: str, package: str, resolution: str) -> dict[str, str]: + return { + "python-version": version, + "package": package, + "import-name": IMPORT_NAMES[package], + "resolution": resolution, + } + + +def matrix_entries(packages: set[str]) -> list[dict[str, str]]: + ordered = [package for package in PACKAGES if package in packages] + entries = [ + matrix_entry(version, package, "highest") + for version in PYTHON_VERSIONS + for package in ordered + ] + entries += [ + matrix_entry(LOWEST_PYTHON_VERSION, package, "lowest-direct") + for package in ordered + ] + return entries + + +def main() -> None: + if "--all" in sys.argv[1:]: + packages = set(PACKAGES) + else: + packages = affected_packages( + [line.strip() for line in sys.stdin if line.strip()] + ) + entries = matrix_entries(packages) + coverage_flags = [ + flag_name(entry) + for entry in matrix_entries(set(PACKAGES)) + if entry["package"] not in PACKAGES_WITHOUT_COVERAGE + ] + print(f"matrix={json.dumps({'include': entries})}") + print(f"has-tests={'true' if entries else 'false'}") + print(f"coverage-flags={','.join(coverage_flags)}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac883c568..71242e595 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,73 +18,118 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true +env: + SUPABASE_CLI_VERSION: "3.0.0" + UV_VERSION: "0.8.2" + jobs: + changes: + name: Select packages to test + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + has-tests: ${{ steps.matrix.outputs.has-tests }} + coverage-flags: ${{ steps.matrix.outputs.coverage-flags }} + steps: + - name: Clone Repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 1 }} + + # A pull request checks out a merge commit, so its first parent is the + # base branch and the diff is exactly what the pull request changes. + - name: Build test matrix + id: matrix + run: | + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + git diff --name-only HEAD^1 HEAD | python3 .github/scripts/ci_matrix.py >> "$GITHUB_OUTPUT" + else + python3 .github/scripts/ci_matrix.py --all >> "$GITHUB_OUTPUT" + fi + cat "$GITHUB_OUTPUT" + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Clone Repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install uv + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 + with: + version: ${{ env.UV_VERSION }} + python-version: "3.11" + + - name: Check lint + run: uv run --frozen ruff check --output-format=github + + - name: Check formatting + run: uv run --frozen ruff format --check + test: - name: py${{ matrix.python-version }} | ${{ matrix.package }} @ ${{ matrix.os }}${{ matrix.resolution == 'lowest-direct' && ' | lowest' || '' }} + name: py${{ matrix.python-version }} | ${{ matrix.package }}${{ matrix.resolution == 'lowest-direct' && ' | lowest' || '' }} + needs: changes + if: ${{ needs.changes.outputs.has-tests == 'true' }} strategy: # Don't let one broken package cancel the other packages' jobs. fail-fast: false - matrix: - os: [ubuntu-latest] - python-version: ["3.11", "3.12", "3.13", "3.14"] - package: ["functions", "realtime", "storage", "auth", "postgrest", "supabase", "utils"] - resolution: ["highest"] - include: - - { os: ubuntu-latest, python-version: "3.11", package: functions, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: realtime, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: storage, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: auth, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: postgrest, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: supabase, resolution: lowest-direct } - - { os: ubuntu-latest, python-version: "3.11", package: utils, resolution: lowest-direct } - runs-on: ${{ matrix.os }} + matrix: ${{ fromJSON(needs.changes.outputs.matrix) }} + runs-on: ubuntu-latest timeout-minutes: 20 + env: + UV_RESOLUTION: ${{ matrix.resolution }} + USES_SUPABASE_CLI: ${{ contains(fromJSON('["realtime", "storage"]'), matrix.package) }} + USES_INFRA: ${{ contains(fromJSON('["realtime", "storage", "auth", "postgrest"]'), matrix.package) }} steps: - name: Clone Repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Install supabase cli latest + - name: Install supabase cli + if: ${{ env.USES_SUPABASE_CLI == 'true' }} uses: supabase/setup-cli@d239d1052f6f024a8d746d7e5aeddead63b9cb53 # v3.1.0 with: - version: "latest" + version: ${{ env.SUPABASE_CLI_VERSION }} - name: Install uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - version: "0.8.2" + version: ${{ env.UV_VERSION }} python-version: ${{ matrix.python-version }} # The supabase cli publishes host ports inside the ephemeral range, and # parallel image pulls can take one as an outgoing source port, which # fails the container start with "address already in use". - name: Reserve infra ports + if: ${{ env.USES_SUPABASE_CLI == 'true' }} run: sudo sysctl -w net.ipv4.ip_local_reserved_ports=54320-54329,55320-55329 - - name: Print supabase cli version - id: supabase-cli - run: echo "version=$(supabase --version | head -n 1)" >> "$GITHUB_OUTPUT" - # Pulling images from the registries on every job gets throttled # (429 / "Data limit exceeded"), so reuse the images between runs. - name: Cache docker images - if: ${{ contains(fromJSON('["realtime", "storage", "auth", "postgrest"]'), matrix.package) }} + if: ${{ env.USES_INFRA == 'true' }} id: docker-images uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/docker-images - key: docker-images-${{ matrix.package }}-${{ steps.supabase-cli.outputs.version }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} + key: docker-images-${{ matrix.package }}-${{ env.SUPABASE_CLI_VERSION }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} - name: Load cached docker images if: ${{ steps.docker-images.outputs.cache-hit == 'true' }} run: zstd -d --stdout /tmp/docker-images/images.tar.zst | docker image load + - name: Install dependencies + run: uv sync --package ${{ matrix.import-name }} + + # Type checking runs while the infra starts, since pytest depends on + # start-infra but not on pyrefly. The environment is already synced, so + # the parallel uv runs only read it. - name: Run Tests - run: make ${{ matrix.package }}.tests - env: - UV_RESOLUTION: ${{ matrix.resolution }} + run: make -j2 --output-sync=target ${{ matrix.package }}.tests + # On a cache miss cache-hit is an empty string, not 'false'. - name: Save docker images to cache - if: ${{ steps.docker-images.outputs.cache-hit == 'false' }} + if: ${{ env.USES_INFRA == 'true' && steps.docker-images.outputs.cache-hit != 'true' }} run: | mkdir -p /tmp/docker-images docker ps --all --format '{{.Image}}' | sort -u \ @@ -97,25 +142,44 @@ jobs: uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7 with: github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ join(matrix.*, '-') }} + flag-name: ${{ matrix.package }}-py${{ matrix.python-version }}-${{ matrix.resolution }} parallel: true fail-on-error: false finish_tests: - needs: test + needs: [changes, test] name: Upload tests coveralls results - if: ${{ always() }} + if: ${{ always() && needs.changes.outputs.has-tests == 'true' }} runs-on: ubuntu-latest steps: + # Packages a pull request does not touch are not tested, so carry their + # coverage forward from the base build. - name: Coveralls Finished uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + carryforward: ${{ needs.changes.outputs.coverage-flags }} fail-on-error: false + ci-passed: + name: CI passed + needs: [changes, lint, test] + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Check job results + env: + CHANGES: ${{ needs.changes.result }} + LINT: ${{ needs.lint.result }} + TEST: ${{ needs.test.result }} + run: | + echo "changes=$CHANGES lint=$LINT test=$TEST" + [ "$CHANGES" = success ] && [ "$LINT" = success ] \ + && { [ "$TEST" = success ] || [ "$TEST" = skipped ]; } + release-please: - needs: test + needs: [lint, test] if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' && github.repository_owner == 'supabase' }} runs-on: ubuntu-latest name: "Run release-please" @@ -160,7 +224,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - version: "0.8.2" + version: ${{ env.UV_VERSION }} python-version: "3.11" - name: Build all packages and publish diff --git a/src/realtime/infra/supabase/config.toml b/src/realtime/infra/supabase/config.toml index d6882737d..d0a943544 100644 --- a/src/realtime/infra/supabase/config.toml +++ b/src/realtime/infra/supabase/config.toml @@ -49,7 +49,7 @@ enabled = true # max_header_length = 4096 [studio] -enabled = true +enabled = false # Port to use for Supabase Studio. port = 54323 # External URL of the API server that frontend connects to. @@ -60,7 +60,7 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. [local_smtp] -enabled = true +enabled = false # Port to use for the email testing server web interface. port = 54324 # Uncomment to expose additional ports for testing user applications that send emails. @@ -68,7 +68,7 @@ port = 54324 # pop3_port = 54326 [storage] -enabled = true +enabled = false # The maximum file size allowed (e.g. "5MB", "500KB"). file_size_limit = "50MiB" @@ -180,14 +180,14 @@ url = "" skip_nonce_check = false [edge_runtime] -enabled = true +enabled = false # Configure one of the supported request policies: `oneshot`, `per_worker`. # Use `oneshot` for hot reload, or `per_worker` for load testing. policy = "oneshot" inspector_port = 8083 [analytics] -enabled = true +enabled = false port = 54327 # Configure one of the supported backends: `postgres`, `bigquery`. backend = "postgres" diff --git a/src/storage/infra/supabase/config.toml b/src/storage/infra/supabase/config.toml index f112ed102..aed0bb5f2 100644 --- a/src/storage/infra/supabase/config.toml +++ b/src/storage/infra/supabase/config.toml @@ -73,7 +73,7 @@ file_size_limit = "50MiB" enabled = true [auth] -enabled = true +enabled = false # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used # in emails. site_url = "http://127.0.0.1:3000" From 2298bc275a52977854cea5adca4edfe433990e1a Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 4 Nov 2026 14:18:19 +0100 Subject: [PATCH 2/3] ci: list both sides of renames when selecting packages to test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71242e595..5b0661225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: id: matrix run: | if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - git diff --name-only HEAD^1 HEAD | python3 .github/scripts/ci_matrix.py >> "$GITHUB_OUTPUT" + git diff --name-only --no-renames HEAD^1 HEAD | python3 .github/scripts/ci_matrix.py >> "$GITHUB_OUTPUT" else python3 .github/scripts/ci_matrix.py --all >> "$GITHUB_OUTPUT" fi From 0b779d5f9cfb72fb8e3129d4fdf2377a857f7cbf Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 4 Nov 2026 14:20:58 +0100 Subject: [PATCH 3/3] ci: use the latest uv and supabase cli --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b0661225..a6d9c10ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true -env: - SUPABASE_CLI_VERSION: "3.0.0" - UV_VERSION: "0.8.2" - jobs: changes: name: Select packages to test @@ -58,7 +54,6 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - version: ${{ env.UV_VERSION }} python-version: "3.11" - name: Check lint @@ -89,12 +84,18 @@ jobs: if: ${{ env.USES_SUPABASE_CLI == 'true' }} uses: supabase/setup-cli@d239d1052f6f024a8d746d7e5aeddead63b9cb53 # v3.1.0 with: - version: ${{ env.SUPABASE_CLI_VERSION }} + version: latest + + # The image versions come from the cli, so a new cli release must not + # reuse images cached by an older one. + - name: Print supabase cli version + if: ${{ env.USES_SUPABASE_CLI == 'true' }} + id: supabase-cli + run: echo "version=$(supabase --version | head -n 1)" >> "$GITHUB_OUTPUT" - name: Install uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - version: ${{ env.UV_VERSION }} python-version: ${{ matrix.python-version }} # The supabase cli publishes host ports inside the ephemeral range, and @@ -112,7 +113,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/docker-images - key: docker-images-${{ matrix.package }}-${{ env.SUPABASE_CLI_VERSION }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} + key: docker-images-${{ matrix.package }}-${{ steps.supabase-cli.outputs.version || 'compose' }}-${{ hashFiles(format('src/{0}/infra/**', matrix.package)) }} - name: Load cached docker images if: ${{ steps.docker-images.outputs.cache-hit == 'true' }} @@ -224,7 +225,6 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: - version: ${{ env.UV_VERSION }} python-version: "3.11" - name: Build all packages and publish