From 6ce0a6914282d4a85b8a142d9bde137cae335a81 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 26 Jun 2026 13:21:50 -0300 Subject: [PATCH 1/6] ci: trigger release-please for v3 branch --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e9aba347..764b24a5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: branches: - main - release/* + - v3 workflow_dispatch: permissions: From a808b783b1b929df61f8be4465660a15ec8bfa72 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 6 Jul 2026 14:53:34 -0300 Subject: [PATCH 2/6] chore: add SwiftLint 0.65.0 with pinned CI version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds SwiftLint as a code-smell/correctness linter alongside swift-format. No changes to Package.swift — downstream consumers are unaffected. Setup: - .swiftlint.yml: all default rules disabled; enable one-by-one in follow-up PRs as violations are fixed - Makefile: adds `make lint` (strict) and `make lint-fix` (autocorrect) - CI: new SwiftLint job pinned to 0.65.0 via portable_swiftlint.zip from GitHub releases, cached by version+OS, version-verified before running. Added to ci-success required-jobs gate. - AGENTS.md: documents the linting workflow Install locally: brew install swiftlint --- .github/workflows/ci.yml | 34 +++++++++++++- .swiftlint.yml | 47 +++++++++++++++++++ AGENTS.md | 16 +++++++ Sources/Helpers/Base64URL.swift | 2 +- .../WebSocket/URLSessionWebSocket.swift | 6 +-- Sources/TestHelpers/URLRequestSnapshot.swift | 4 +- .../FunctionsTests/FunctionsClientTests.swift | 5 -- .../RealtimeIntegrationTests.swift | 4 +- .../PostgrestFilterBuilderTests.swift | 5 -- .../PostgrestQueryBuilderTests.swift | 4 -- Tests/RealtimeTests/RealtimeTests.swift | 2 +- Tests/StorageTests/StorageFileAPITests.swift | 4 +- 12 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 .swiftlint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9425750d3..493a29f9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,10 +321,42 @@ jobs: echo "✅ All changed Swift files are properly formatted" fi + lint: + name: SwiftLint + runs-on: macos-26 + timeout-minutes: 10 + env: + SWIFTLINT_VERSION: "0.65.0" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Cache SwiftLint binary + id: cache-swiftlint + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: /usr/local/bin/swiftlint + key: swiftlint-${{ env.SWIFTLINT_VERSION }}-${{ runner.os }} + - name: Install SwiftLint ${{ env.SWIFTLINT_VERSION }} + if: steps.cache-swiftlint.outputs.cache-hit != 'true' + run: | + curl -fL \ + "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \ + -o /tmp/swiftlint.zip + unzip -q -o /tmp/swiftlint.zip -d /tmp/swiftlint + sudo mv /tmp/swiftlint/swiftlint /usr/local/bin/swiftlint + - name: Verify SwiftLint version + run: | + INSTALLED=$(swiftlint version) + if [ "$INSTALLED" != "$SWIFTLINT_VERSION" ]; then + echo "Expected SwiftLint $SWIFTLINT_VERSION, got $INSTALLED" && exit 1 + fi + echo "SwiftLint $INSTALLED" + - name: Run SwiftLint + run: make lint + ci-success: name: CI Success if: always() - needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check] + needs: [macos, macos-legacy, spm, linux, integration-tests, library-evolution, examples, spell-check, docs, format-check, lint] runs-on: ubuntu-latest steps: - name: Check all jobs diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 000000000..d1aa10eb2 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +# SwiftLint configuration for supabase-swift +# https://github.com/realm/SwiftLint +# +# Pinned version: 0.65.0 +# Install locally: brew install swiftlint +# CI pins the exact binary via portable_swiftlint.zip from GitHub releases. +# +# Only rules that currently trigger violations are disabled. Enable them +# one-by-one in follow-up PRs once the violations are fixed or configured. + +included: + - Sources + - Tests + +excluded: + - .build + - Tests/IntegrationTests/supabase + - "**/__Snapshots__" + +# Rules disabled because they currently have violations. +# Remove entries here as violations are fixed in follow-up PRs. +disabled_rules: + - class_delegate_protocol + - closure_parameter_position + - comment_spacing + - cyclomatic_complexity + - file_length + - for_where + - force_cast + - force_try + - function_body_length + - function_name_whitespace + - function_parameter_count + - identifier_name + - implicit_optional_initialization + - line_length + - multiple_closures_with_trailing_closure + - nesting + - non_optional_string_data_conversion + - opening_brace + - optional_data_string_conversion + - orphaned_doc_comment + - redundant_discardable_let + - todo + - trailing_comma + - type_body_length + - type_name diff --git a/AGENTS.md b/AGENTS.md index d268685ea..d15781127 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,21 @@ npm ci --prefix tools/node # one-time setup (re-run only when tools/node/packa Legitimate technical terms and project-specific words go in `dictionary.txt` at the repository root. +### Linting + +```bash +# Lint Sources and Tests (fails on any new violation) +make lint + +# Autocorrect violations that SwiftLint can fix +make lint-fix +``` + +This uses [SwiftLint](https://github.com/realm/SwiftLint) for code-smell and +correctness rules; `swift-format` remains the source of truth for formatting, +so the SwiftLint config (`.swiftlint.yml`) disables the purely stylistic rules +that overlap with it. Install SwiftLint locally with `brew install swiftlint`. + ### Documentation ```bash @@ -297,6 +312,7 @@ supabase stop ## Important Notes for AI Coding Agents - Always run `./scripts/format.sh` before committing Swift code +- Run `swiftlint lint --strict` before committing; it must not report new violations - Ensure new public APIs have DocC documentation comments - Add tests for all new functionality - Keep changes minimal and focused diff --git a/Sources/Helpers/Base64URL.swift b/Sources/Helpers/Base64URL.swift index daf3fec9a..3e19f3de8 100644 --- a/Sources/Helpers/Base64URL.swift +++ b/Sources/Helpers/Base64URL.swift @@ -17,7 +17,7 @@ package enum Base64URL { let paddingLength = requiredLength - length if paddingLength > 0 { let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - base64 = base64 + padding + base64 += padding } return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) } diff --git a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift index 9a4c50f18..20dc3e290 100644 --- a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift +++ b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift @@ -74,7 +74,7 @@ final class URLSessionWebSocket: WebSocket { let session = URLSession.sessionWithConfiguration( configuration ?? .default, - onComplete: { session, task, error in + onComplete: { _, _, error in mutableState.withValue { if let webSocket = $0.webSocket { // There are three possibilities here: @@ -104,14 +104,14 @@ final class URLSessionWebSocket: WebSocket { } } }, - onWebSocketTaskOpened: { session, task, `protocol` in + onWebSocketTaskOpened: { _, task, `protocol` in mutableState.withValue { $0.webSocket = URLSessionWebSocket( _task: task, _protocol: `protocol` ?? "", session: session) $0.continuation.resume(returning: $0.webSocket!) } }, - onWebSocketTaskClosed: { session, task, code, reason in + onWebSocketTaskClosed: { _, _, code, reason in mutableState.withValue { assert($0.webSocket != nil, "connection should exist by this time") $0.webSocket!._connectionClosed(code: code, reason: reason) diff --git a/Sources/TestHelpers/URLRequestSnapshot.swift b/Sources/TestHelpers/URLRequestSnapshot.swift index e9e59e0f6..7c69cd5df 100644 --- a/Sources/TestHelpers/URLRequestSnapshot.swift +++ b/Sources/TestHelpers/URLRequestSnapshot.swift @@ -15,8 +15,8 @@ #endif extension Snapshotting where Value == URLRequest, Format == String { - /// A snapshot strategy for comparing requests based on a cURL representation. - /// + // A snapshot strategy for comparing requests based on a cURL representation. + // // ``` swift // assertSnapshot(of: request, as: .curl) // ``` diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index ae72229f3..1bf327e9b 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -38,11 +38,6 @@ final class FunctionsClientTests: XCTestCase { sessionConfiguration: sessionConfiguration ) - override func setUp() { - super.setUp() - // isRecording = true - } - func testInit() async { let client = FunctionsClient( url: url, diff --git a/Tests/IntegrationTests/RealtimeIntegrationTests.swift b/Tests/IntegrationTests/RealtimeIntegrationTests.swift index 126cb3829..7d2343b15 100644 --- a/Tests/IntegrationTests/RealtimeIntegrationTests.swift +++ b/Tests/IntegrationTests/RealtimeIntegrationTests.swift @@ -785,7 +785,7 @@ // Verify both clients can decode presence // Note: Due to timing, exact presence changes may vary, but structure should be correct - XCTAssertTrue(presenceChanges1.count > 0, "Client 1 should receive presence changes") + XCTAssertTrue(!presenceChanges1.isEmpty, "Client 1 should receive presence changes") // Verify messages were received by both clients XCTAssertEqual(messages1.count, 3, "Client 1 should receive all 3 messages") @@ -816,7 +816,7 @@ // Verify user 1 leaving is detected by user 2 // Note: Due to timing, exact presence changes may vary, but structure should be correct - XCTAssertTrue(presenceChanges2.count > 0, "Client 2 should receive presence changes") + XCTAssertTrue(!presenceChanges2.isEmpty, "Client 2 should receive presence changes") // Cleanup await channel1.unsubscribe() diff --git a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift index e9787894a..f5eb9bb83 100644 --- a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift @@ -12,11 +12,6 @@ import XCTest final class PostgrestFilterBuilderTests: PostgrestQueryTests { - override func setUp() { - super.setUp() - // isRecording = true - } - func testNotFilter() async throws { Mock( url: url.appendingPathComponent("users"), diff --git a/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift b/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift index cb8d935ec..b0d356756 100644 --- a/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestQueryBuilderTests.swift @@ -12,10 +12,6 @@ import TestHelpers import XCTest final class PostgrestQueryBuilderTests: PostgrestQueryTests { - override func setUp() { - super.setUp() - // isRecording = true - } func testSetAuth() { XCTAssertNil(sut.configuration.headers["Authorization"]) diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 1864c00f6..b6440ed37 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -76,7 +76,7 @@ import XCTest "custom.access.token" } ), - wsTransport: { url, headers in + wsTransport: { url, _ in assertInlineSnapshot(of: url, as: .description) { """ ws://localhost:54321/realtime/v1/websocket?apikey=publishable.api.key&vsn=2.0.0&log_level=warn diff --git a/Tests/StorageTests/StorageFileAPITests.swift b/Tests/StorageTests/StorageFileAPITests.swift index 3fd915502..22da4ce07 100644 --- a/Tests/StorageTests/StorageFileAPITests.swift +++ b/Tests/StorageTests/StorageFileAPITests.swift @@ -454,7 +454,7 @@ final class StorageFileAPITests: XCTestCase { do { try await storage.from("bucket") .move(from: "source", to: "destination") - XCTFail() + XCTFail("Expected StorageError to be thrown") } catch let error as StorageError { XCTAssertEqual(error.message, "Error") } @@ -485,7 +485,7 @@ final class StorageFileAPITests: XCTestCase { do { try await storage.from("bucket") .move(from: "source", to: "destination") - XCTFail() + XCTFail("Expected HTTPError to be thrown") } catch let error as HTTPError { XCTAssertEqual(error.data, Data("error".utf8)) XCTAssertEqual(error.response.statusCode, 412) From 69e2e21f4e6302e174c8dfeb986d10c1f4cc54a8 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Jul 2026 05:28:37 -0300 Subject: [PATCH 3/6] fix: use swiftlint directly in CI and restore URLSession parameter name --- .github/workflows/ci.yml | 2 +- Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 493a29f9f..17cd662d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,7 +351,7 @@ jobs: fi echo "SwiftLint $INSTALLED" - name: Run SwiftLint - run: make lint + run: swiftlint lint --strict ci-success: name: CI Success diff --git a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift index 20dc3e290..d09c187f3 100644 --- a/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift +++ b/Sources/RealtimeV2/WebSocket/URLSessionWebSocket.swift @@ -104,7 +104,7 @@ final class URLSessionWebSocket: WebSocket { } } }, - onWebSocketTaskOpened: { _, task, `protocol` in + onWebSocketTaskOpened: { session, task, `protocol` in mutableState.withValue { $0.webSocket = URLSessionWebSocket( _task: task, _protocol: `protocol` ?? "", session: session) From 42936b1e8156c21053d6f3e3daaf721c87d3ad38 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Jul 2026 05:32:59 -0300 Subject: [PATCH 4/6] chore: add swiftlint to spell-check dictionary --- dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dictionary.txt b/dictionary.txt index 7858a3f3a..32d67b95a 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -137,6 +137,7 @@ Supabase supabase SUPABASE supabot +swiftlint testpath testuser timestamptz From 1c1581c7ef82c91bb24b583782ef8c4e017e7247 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Jul 2026 06:46:52 -0300 Subject: [PATCH 5/6] docs(agents): use swiftlint directly instead of make targets --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d15781127..41e9d20a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,10 +90,10 @@ Legitimate technical terms and project-specific words go in `dictionary.txt` at ```bash # Lint Sources and Tests (fails on any new violation) -make lint +swiftlint lint --strict # Autocorrect violations that SwiftLint can fix -make lint-fix +swiftlint lint --fix ``` This uses [SwiftLint](https://github.com/realm/SwiftLint) for code-smell and From 25e45192fff5fb25b9c7675c021d72e29f801dc6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 8 Jul 2026 05:59:02 -0300 Subject: [PATCH 6/6] ci: pin SwiftLint zip checksum for supply-chain safety --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17cd662d4..a32f35b71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -327,6 +327,7 @@ jobs: timeout-minutes: 10 env: SWIFTLINT_VERSION: "0.65.0" + SWIFTLINT_SHA256: "d6cb0aa7a2f5f1ef306fc9e37bcb54dc9a26facc8f7784ac0c3dd3eccf5c6ba6" steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Cache SwiftLint binary @@ -341,6 +342,7 @@ jobs: curl -fL \ "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \ -o /tmp/swiftlint.zip + echo "${SWIFTLINT_SHA256} /tmp/swiftlint.zip" | shasum -a 256 --check unzip -q -o /tmp/swiftlint.zip -d /tmp/swiftlint sudo mv /tmp/swiftlint/swiftlint /usr/local/bin/swiftlint - name: Verify SwiftLint version