From e758d54cf0623dc832d7370c4320b2bd5d9a7efa Mon Sep 17 00:00:00 2001 From: Matilde Enevoldsen Date: Tue, 3 Nov 2026 11:03:04 +0000 Subject: [PATCH 1/2] Add standalone SDK checks and contribution workflow --- .gitattributes | 3 + .github/dependabot.yml | 11 +++ .github/pull_request_template.md | 14 ++++ .github/workflows/ci.yml | 65 ++++++++++++++++++ .gitignore | 6 ++ CONTRIBUTING.md | 71 ++++++++++++------- composer.json | 32 +++++++++ docs/review/sdk-ci.md | 32 +++++++++ phpstan.neon | 10 +++ phpunit.xml | 23 +++++++ pint.json | 58 ++++++++++++++++ src/Laravel/Commands/ReleaseCommand.php | 3 - src/Laravel/Commands/SymbolsCommand.php | 2 +- tests/ClientTest.php | 4 +- tests/Fixtures/Symbols/CrashFixture.dwarf | Bin 0 -> 12760 bytes tests/Fixtures/Symbols/CrashFixture.swift | 7 ++ tests/Fixtures/Symbols/README.md | 28 ++++++++ tests/Pest.php | 75 +++++++++++++++++++++ tests/StackFramesTest.php | 8 +-- tests/SymbolsCommandTest.php | 2 +- tests/TestCase.php | 26 +++++++ tests/Watchers/NotificationWatcherTest.php | 5 +- 22 files changed, 450 insertions(+), 35 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 docs/review/sdk-ci.md create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 tests/Fixtures/Symbols/CrashFixture.dwarf create mode 100644 tests/Fixtures/Symbols/CrashFixture.swift create mode 100644 tests/Fixtures/Symbols/README.md create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php diff --git a/.gitattributes b/.gitattributes index 9dec809..bdab6cb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,6 @@ /AGENTS.md export-ignore /CLAUDE.md export-ignore /.github export-ignore +/phpunit.xml export-ignore +/phpstan.neon export-ignore +/pint.json export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..029e094 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ff27d52 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## What changes and why + +Describe the problem and the smallest complete solution. + +## Evidence + +List the commands run and their results. For native changes, include the real +compiled app, crash/relaunch results and screenshots, identifying the platform +and whether it was a simulator or physical device. CI does not prove native +crash capture. + +## Compatibility + +Describe public API, event format or minimum-version changes, if any. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..86222d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + COMPOSER_ROOT_VERSION: 0.2.x-dev + +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2 + with: + php-version: '8.4' + extensions: mbstring, pdo_sqlite + tools: composer:v2 + coverage: none + - run: composer validate --strict + - run: composer update --no-interaction --prefer-dist --no-progress + - run: composer audit + - run: composer test:lint + - run: composer test:types + - run: composer test:type-coverage + + tests: + name: Tests (PHP ${{ matrix.php }}, Laravel ${{ matrix.laravel }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] + laravel: ['11', '12', '13'] + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, pdo_sqlite + tools: composer:v2 + coverage: none + - name: Select Laravel version + env: + LARAVEL_VERSION: ${{ matrix.laravel }} + run: composer require --dev "orchestra/testbench:^$((LARAVEL_VERSION - 2)).0" --no-update --no-interaction + - run: composer update --no-interaction --prefer-dist --no-progress + - run: composer test:unit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bcc21a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/composer.lock +/.phpunit.cache/ +/.phpunit.result.cache +/.phpstan-cache/ +/.php-cs-fixer.cache diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce81a6a..b0c83e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,30 +1,55 @@ -# Contributing to the NativeWatch SDKs +# Contributing -Use PHP 8.4 or newer and Composer 2. Open an issue in the affected SDK repository -before a substantial feature or a change to the event format. Include the SDK, -Laravel and NativePHP versions, a minimal reproduction and redacted logs. +Use PHP 8.4 or newer and Composer 2. Open an issue before a substantial feature +or event-format change. Include SDK, Laravel and NativePHP versions, a minimal +reproduction and redacted logs. Never publish credentials or customer reports. +Report vulnerabilities privately to a repository maintainer. -Shared capture, redaction and delivery belong in `native-core`. Desktop and -mobile integrations belong in their platform packages. Keep changes focused, -classes final, types explicit and files within 300 lines. Preserve the original -application error when reporting fails. Never include credentials or customer -crash reports in an issue, test fixture or pull request. +Shared capture, redaction and delivery belong in `native-core`. Platform hooks +belong in `native-desktop` or `native-mobile`. Keep changes focused, classes +final, types explicit and files within 300 lines. Reporting must never replace +the original application error. -Validate a package checkout with: +## Run the checks ```sh -composer validate --strict -composer install +COMPOSER_ROOT_VERSION=0.2.x-dev composer install +composer test ``` -The SDK regression tests run with the server's integration suite in the -maintainers' checkout. A standalone `composer install` does not run those tests. -Include a regression test or reproduction with a fix; maintainers run the full -suite before merging. Changes to native reporting also need a compiled Release -app, a real crash and relaunch, server-event assertions and screenshots. Identify -the platform and distinguish simulator evidence from a physical device. - -In a pull request, explain the failing behavior, the smallest complete fix and -the commands you actually ran. Include compatibility changes and any remaining -limits. Report suspected vulnerabilities privately to a repository maintainer -instead of opening an issue containing exploit details or private data. +The root version identifies the development checkout as part of the 0.2 line. +Composer will resolve a lock file locally; libraries do not commit it. Tests use +Orchestra Testbench and fake HTTP, without the private server, production keys +or a running NativePHP app. `composer test` runs Pint, PHPStan, 100% PHP type coverage and Pest. GitHub +also validates Composer metadata, audits dependencies and runs Pest on PHP 8.4 +and 8.5 with Laravel 11, 12 and 13. + +For one Laravel version, select its matching Testbench before installing: + +```sh +composer require --dev 'orchestra/testbench:^9.0' --no-update +COMPOSER_ROOT_VERSION=0.2.x-dev composer update +composer test +``` + +Testbench 9 tests Laravel 11; Testbench 10 tests Laravel 12; Testbench 11 tests +Laravel 13. Do not commit a local matrix selection to `composer.json`. + +Native reporting changes also require a compiled Release app, a real crash and +relaunch, receiver assertions and screenshots. Identify the platform and +whether evidence comes from a simulator or physical device. PHP CI does not +compile Swift or Kotlin or establish native crash capture. + +## Submit a pull request + +Explain the failing behavior, the smallest complete fix and the commands you +actually ran. Include a regression test, compatibility changes and remaining +limits. Use a branch and open a pull request against `main`. Merging requires +one approving review from another maintainer, resolved conversations and all +required CI checks passing against the latest `main`. New commits dismiss old +approvals. Force pushes and deletion of `main` are blocked. + +Workflow actions are pinned to full commit hashes and updated by Dependabot. +Keep workflow permissions read-only and never expose secrets to forked code or +use `pull_request_target` to execute a contributor's checkout. Published `v*` +tags cannot be rewritten or deleted. Fix a release with a new version. diff --git a/composer.json b/composer.json index c11a79e..f002a3d 100644 --- a/composer.json +++ b/composer.json @@ -21,5 +21,37 @@ "docs": "https://native.watch/docs", "issues": "https://github.com/zinxan/native.core/issues", "source": "https://github.com/zinxan/native.core" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/pint": "^1.27", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.8|^4.0|^5.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.0|^5.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0|^5.0", + "zinxan/native-desktop": "^0.2" + }, + "autoload-dev": { + "psr-4": { + "ZXN\\NativeWatch\\Core\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + }, + "sort-packages": true + }, + "scripts": { + "test": [ + "@test:lint", + "@test:types", + "@test:type-coverage", + "@test:unit" + ], + "test:lint": "pint --test", + "test:types": "phpstan analyse --memory-limit=1G", + "test:unit": "pest --compact", + "test:type-coverage": "pest --type-coverage --min=100" } } diff --git a/docs/review/sdk-ci.md b/docs/review/sdk-ci.md new file mode 100644 index 0000000..5a599c2 --- /dev/null +++ b/docs/review/sdk-ci.md @@ -0,0 +1,32 @@ +# Standalone SDK CI evidence + +Verified on 3 November 2026 with PHP 8.4.27 and Laravel 13. + +- `composer validate --strict`: passed. +- `composer audit`: no advisories or abandoned dependencies. +- `composer test`: Pint and PHPStan level 7 passed, 100% PHP type coverage, + 110 Pest tests and 542 assertions passed. +- `git diff --check`: passed. + +The workflow repeats Pest across PHP 8.4/8.5 and Laravel 11/12/13. Its Quality +job validates Composer, audits dependencies and runs lint, static analysis and +type coverage. Every job runs for every pull request, without path filters. +Both Actions are pinned to full commit hashes; checkout credentials are not +persisted, the token is read-only and the workflow uses no repository secrets. + +The public suite runs against Orchestra Testbench rather than the private +NativeWatch server. Server acceptance and end-to-end native crash behavior +still require their respective integration checks. This CI does not compile +or execute Swift, Kotlin, an iOS simulator or an Android device. + +The first standalone run exposed 15 failures caused by private-server fixture, +model, schema and path dependencies. Those are replaced by a package-local +compiled Swift fixture, a notifiable Laravel model and package-relative paths. +The long-message test asserts the 8,192-character limit and valid UTF-8; +validation against the server's complete FormRequest stays in its integration +suite. Existing capture, consent, retry, watcher, release and symbol-upload +regressions run without a production endpoint. + +PHPStan also found redundant type guards around scalar console inputs whose +Laravel command signatures already enforce their type. Only those guards were +removed; optional source-root and CI credential validation remain in place. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..c236e86 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + level: 7 + paths: + - src + tmpDir: .phpstan-cache + dynamicConstantNames: + - FILTER_FLAG_GLOBAL_RANGE diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7efc43c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + tests + + + + src + + + + + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..d4c8c61 --- /dev/null +++ b/pint.json @@ -0,0 +1,58 @@ +{ + "preset": "laravel", + "rules": { + "array_push": true, + "backtick_to_shell_exec": true, + "date_time_immutable": true, + "declare_strict_types": true, + "lowercase_keywords": true, + "lowercase_static_reference": true, + "final_class": true, + "final_internal_class": true, + "final_public_method_for_abstract_class": true, + "fully_qualified_strict_types": true, + "global_namespace_import": { + "import_classes": true, + "import_constants": true, + "import_functions": true + }, + "mb_str_functions": true, + "modernize_types_casting": true, + "new_with_parentheses": false, + "no_superfluous_elseif": true, + "no_useless_else": true, + "no_multiple_statements_per_line": true, + "ordered_class_elements": { + "order": [ + "use_trait", + "case", + "constant", + "constant_public", + "constant_protected", + "constant_private", + "property_public", + "property_protected", + "property_private", + "construct", + "destruct", + "magic", + "phpunit", + "method_abstract", + "method_public_static", + "method_public", + "method_protected_static", + "method_protected", + "method_private_static", + "method_private" + ], + "sort_algorithm": "none" + }, + "ordered_interfaces": true, + "ordered_traits": true, + "protected_to_private": true, + "self_accessor": true, + "self_static_accessor": true, + "strict_comparison": true, + "visibility_required": true + } +} diff --git a/src/Laravel/Commands/ReleaseCommand.php b/src/Laravel/Commands/ReleaseCommand.php index 7a15be2..5258fd5 100644 --- a/src/Laravel/Commands/ReleaseCommand.php +++ b/src/Laravel/Commands/ReleaseCommand.php @@ -23,9 +23,6 @@ public function handle(BuildReleaseManifest $builder, CiConnection $connection): { $path = $this->option('path') ?? base_path(); $version = $this->argument('version'); - if (! is_string($path) || ! is_string($version)) { - return self::FAILURE; - } try { if ($this->option('id-only')) { $this->line($builder->buildId($path, $version)); diff --git a/src/Laravel/Commands/SymbolsCommand.php b/src/Laravel/Commands/SymbolsCommand.php index d7aa2ff..7e58544 100644 --- a/src/Laravel/Commands/SymbolsCommand.php +++ b/src/Laravel/Commands/SymbolsCommand.php @@ -23,7 +23,7 @@ public function handle(FindDebugSymbols $find, CiConnection $connection): int try { $path = $this->argument('path'); $root = $this->option('source-root'); - if (! is_string($path) || ! is_string($root) || $root === '') { + if (! is_string($root) || $root === '') { throw new RuntimeException('Pass --source-root with the absolute checkout root used to compile this build.'); } $request = $connection->request(); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index ab47937..0b71086 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -175,7 +175,7 @@ && $request['breadcrumbs'] === []); }); -it('keeps long exception messages within the ingest contract', function (bool $native): void { +it('truncates long exception messages without splitting UTF-8 characters', function (bool $native): void { Http::fake(['zxguard.test/*' => Http::response(status: 202)]); $client = client(); $message = str_repeat('é', 8_193); @@ -189,5 +189,5 @@ $client->flush(); Http::assertSent(fn (Request $request): bool => mb_strlen($request['exception']['value']) === 8_192 - && validator($request->data(), (new App\Http\Requests\Api\V1\StoreOccurrenceRequest)->rules())->passes()); + && mb_check_encoding($request['exception']['value'], 'UTF-8')); })->with([false, true]); diff --git a/tests/Fixtures/Symbols/CrashFixture.dwarf b/tests/Fixtures/Symbols/CrashFixture.dwarf new file mode 100644 index 0000000000000000000000000000000000000000..56990d0699a08dc18f8fc8a71a01668e77d082b3 GIT binary patch literal 12760 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 literal 0 HcmV?d00001 diff --git a/tests/Fixtures/Symbols/CrashFixture.swift b/tests/Fixtures/Symbols/CrashFixture.swift new file mode 100644 index 0000000..a8667d1 --- /dev/null +++ b/tests/Fixtures/Symbols/CrashFixture.swift @@ -0,0 +1,7 @@ +@inline(never) +func nativeWatchFixtureFailure(_ value: Int) -> Int { + precondition(value != 0, "NativeWatch symbolication fixture") + return 84 / value +} + +print(nativeWatchFixtureFailure(CommandLine.arguments.count - 1)) diff --git a/tests/Fixtures/Symbols/README.md b/tests/Fixtures/Symbols/README.md new file mode 100644 index 0000000..7bae467 --- /dev/null +++ b/tests/Fixtures/Symbols/README.md @@ -0,0 +1,28 @@ +# Compiled Swift symbol fixture + +`CrashFixture.dwarf` is real optimized Mach-O debug information built from +`CrashFixture.swift`, using Xcode 27.1. It is not a fabricated symbol response. +The tests run the installed LLVM symbolizer and official Swift demangler against +its bytes on macOS and Linux CI. + +Recorded identity: `bf2dbb4a-b545-3baa-a8a7-8b40b5ea12c5`, arm64, +preferred image base `0x100000000`. Offset `0x714` resolves to +`nativeWatchFixtureFailure`, source line 4. A return address of `0x718` resolves +to the same instruction after the architecture adjustment. + +To rebuild from the repository root on an Apple Silicon Mac: + +```sh +xcrun swiftc -c -g -O -module-name NativeWatchSymbolFixture \ + -debug-prefix-map "$PWD=/nativewatch-fixtures" \ + tests/Fixtures/Symbols/CrashFixture.swift -o /tmp/nativewatch-symbol-fixture.o +xcrun swiftc -g /tmp/nativewatch-symbol-fixture.o -o /tmp/nativewatch-symbol-fixture +xcrun dsymutil /tmp/nativewatch-symbol-fixture -o /tmp/nativewatch-symbol-fixture.dSYM +cp /tmp/nativewatch-symbol-fixture.dSYM/Contents/Resources/DWARF/nativewatch-symbol-fixture \ + tests/Fixtures/Symbols/CrashFixture.dwarf +xcrun dwarfdump --uuid tests/Fixtures/Symbols/CrashFixture.dwarf +``` + +A rebuild can change UUIDs and offsets; update the corresponding assertions +from actual tool output. Prefix remapping keeps developer home paths out of +this fixture. This fixture proves symbol resolution, not crash capture. diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..dfac152 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,75 @@ +extend(TestCase::class)->in(__DIR__); + +function temporaryOutbox(): string +{ + $path = sys_get_temp_dir().'/nativewatch-tests/'.bin2hex(random_bytes(6)); + mkdir($path, 0755, true); + + return $path; +} + +/** + * Get the activity entries that the SDK sent, after the app finished its work. + * + * @return list> + */ +function sentActivities(?string $signal = null): array +{ + app()->terminate(); + + $entries = []; + + foreach (Illuminate\Support\Facades\Http::recorded() as [$request]) { + if (str_ends_with($request->url(), '/api/v1/activities')) { + array_push($entries, ...$request['entries']); + } + } + + return array_values(array_filter($entries, fn (array $entry): bool => $signal === null || $entry['signal'] === $signal)); +} + +/** + * Get the events that the SDK sent, after the app finished its work. + * + * @return list> + */ +function sentOccurrences(): array +{ + app()->terminate(); + + $events = []; + + foreach (Illuminate\Support\Facades\Http::recorded() as [$request]) { + if (str_ends_with($request->url(), '/api/v1/occurrences')) { + $events[] = $request->data(); + } + } + + return $events; +} + +/** + * Register the desktop SDK with a fake NativeWatch server, for watcher tests. + * + * @param array $config + */ +function fakeNativeWatch(array $config = []): void +{ + Illuminate\Support\Facades\Http::fake(['zxguard.test/*' => Illuminate\Support\Facades\Http::response(status: 202)]); + + config([ + 'nativewatch.key' => 'nw_test', + 'nativewatch.url' => 'https://zxguard.test', + 'nativewatch.outbox.path' => temporaryOutbox(), + 'nativewatch.consent.path' => temporaryOutbox().'/consent.json', + ...$config, + ]); + + app()->register(ZXN\NativeWatch\Desktop\DesktopServiceProvider::class); +} diff --git a/tests/StackFramesTest.php b/tests/StackFramesTest.php index 4fb381e..bb9df16 100644 --- a/tests/StackFramesTest.php +++ b/tests/StackFramesTest.php @@ -6,12 +6,12 @@ it('starts with the line that threw and marks frames of the app', function (): void { $exception = new RuntimeException('Boom'); - $basePath = dirname(__DIR__, 3); + $basePath = dirname(__DIR__); $frames = new StackFrames($basePath)->fromThrowable($exception); expect($frames[0]) - ->file->toBe('packages/native-core/tests/StackFramesTest.php') + ->file->toBe('tests/StackFramesTest.php') ->line->toBe(__LINE__ - 7) ->in_app->toBeTrue(); @@ -32,7 +32,7 @@ $line = __LINE__ + 1; $exception = new RuntimeException('Boom'); - $frames = new StackFrames(dirname(__DIR__, 3), codeLines: 2)->fromThrowable($exception); + $frames = new StackFrames(dirname(__DIR__), codeLines: 2)->fromThrowable($exception); $code = $frames[0]['code']; expect($code)->not->toBeNull() @@ -46,7 +46,7 @@ }); it('sends no source code when it is turned off', function (): void { - $frames = new StackFrames(dirname(__DIR__, 3), codeLines: 0)->fromThrowable(new RuntimeException('Boom')); + $frames = new StackFrames(dirname(__DIR__), codeLines: 0)->fromThrowable(new RuntimeException('Boom')); expect(array_column($frames, 'code'))->each->toBeNull(); }); diff --git a/tests/SymbolsCommandTest.php b/tests/SymbolsCommandTest.php index d0c3a9d..38747a9 100644 --- a/tests/SymbolsCommandTest.php +++ b/tests/SymbolsCommandTest.php @@ -12,7 +12,7 @@ $this->app->register(DesktopServiceProvider::class); config(['nativewatch.url' => 'http://zxguard.test']); putenv('NATIVEWATCH_RELEASE_TOKEN=nwr_ci-symbol-secret'); - $this->dwarf = __DIR__.'/../../../tests/Fixtures/Symbols/CrashFixture.dwarf'; + $this->dwarf = __DIR__.'/Fixtures/Symbols/CrashFixture.dwarf'; $this->receipt = ['symbols' => [['imageUuid' => 'bf2dbb4a-b545-3baa-a8a7-8b40b5ea12c5', 'architecture' => ['value' => 'arm64'], 'sha256' => hash_file('sha256', $this->dwarf)]]]; }); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..2e42f83 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,26 @@ +set([ + 'app.key' => 'base64:'.base64_encode(str_repeat('a', 32)), + 'cache.default' => 'array', + 'database.default' => 'testing', + 'database.connections.testing' => ['driver' => 'sqlite', 'database' => ':memory:'], + 'mail.default' => 'array', + 'queue.default' => 'sync', + 'session.driver' => 'array', + 'nativewatch.key' => null, + ]); + } +} diff --git a/tests/Watchers/NotificationWatcherTest.php b/tests/Watchers/NotificationWatcherTest.php index 3c0d7ab..6e529ac 100644 --- a/tests/Watchers/NotificationWatcherTest.php +++ b/tests/Watchers/NotificationWatcherTest.php @@ -27,7 +27,10 @@ public function toArray(object $notifiable): array it('records sent and failed notifications with their channel', function (): void { fakeNativeWatch(); - $user = App\Models\User::factory()->make(); + $user = new class extends Illuminate\Foundation\Auth\User + { + use Illuminate\Notifications\Notifiable; + }; Illuminate\Support\Facades\Schema::create('notifications', function (Illuminate\Database\Schema\Blueprint $table): void { $table->uuid('id')->primary(); From 5cb976f9339a5269d9b9863e32ea69574eae4b20 Mon Sep 17 00:00:00 2001 From: Matilde Enevoldsen Date: Tue, 3 Nov 2026 11:06:13 +0000 Subject: [PATCH 2/2] Keep security checks enabled for supported test dependencies --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 11 +++++++---- composer.json | 2 +- docs/review/sdk-ci.md | 7 ++++++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86222d1..9ef56bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: fail-fast: false matrix: php: ['8.4', '8.5'] - laravel: ['11', '12', '13'] + laravel: ['12', '13'] steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0c83e6..0d1e3c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,18 +22,21 @@ Composer will resolve a lock file locally; libraries do not commit it. Tests use Orchestra Testbench and fake HTTP, without the private server, production keys or a running NativePHP app. `composer test` runs Pint, PHPStan, 100% PHP type coverage and Pest. GitHub also validates Composer metadata, audits dependencies and runs Pest on PHP 8.4 -and 8.5 with Laravel 11, 12 and 13. +and 8.5 with Laravel 12 and 13. For one Laravel version, select its matching Testbench before installing: ```sh -composer require --dev 'orchestra/testbench:^9.0' --no-update +composer require --dev 'orchestra/testbench:^10.0' --no-update COMPOSER_ROOT_VERSION=0.2.x-dev composer update composer test ``` -Testbench 9 tests Laravel 11; Testbench 10 tests Laravel 12; Testbench 11 tests -Laravel 13. Do not commit a local matrix selection to `composer.json`. +Testbench 10 tests Laravel 12; Testbench 11 tests Laravel 13. Do not commit a local matrix selection to `composer.json`. + +The runtime dependency constraint still accepts Laravel 11, but CI does not +verify it: Composer blocks its current releases due to security advisories. +No advisory ignores or disabled security gates are used. Native reporting changes also require a compiled Release app, a real crash and relaunch, receiver assertions and screenshots. Identify the platform and diff --git a/composer.json b/composer.json index f002a3d..1c61c05 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require-dev": { "larastan/larastan": "^3.9", "laravel/pint": "^1.27", - "orchestra/testbench": "^9.0|^10.0|^11.0", + "orchestra/testbench": "^10.0|^11.0", "pestphp/pest": "^3.8|^4.0|^5.0", "pestphp/pest-plugin-laravel": "^3.0|^4.0|^5.0", "pestphp/pest-plugin-type-coverage": "^3.0|^4.0|^5.0", diff --git a/docs/review/sdk-ci.md b/docs/review/sdk-ci.md index 5a599c2..84fbb76 100644 --- a/docs/review/sdk-ci.md +++ b/docs/review/sdk-ci.md @@ -8,7 +8,7 @@ Verified on 3 November 2026 with PHP 8.4.27 and Laravel 13. 110 Pest tests and 542 assertions passed. - `git diff --check`: passed. -The workflow repeats Pest across PHP 8.4/8.5 and Laravel 11/12/13. Its Quality +The workflow repeats Pest across PHP 8.4/8.5 and Laravel 12/13. Its Quality job validates Composer, audits dependencies and runs lint, static analysis and type coverage. Every job runs for every pull request, without path filters. Both Actions are pinned to full commit hashes; checkout credentials are not @@ -30,3 +30,8 @@ regressions run without a production endpoint. PHPStan also found redundant type guards around scalar console inputs whose Laravel command signatures already enforce their type. Only those guards were removed; optional source-root and CI credential validation remain in place. + +The initial GitHub Laravel 11 matrix jobs were blocked by Composer security +advisories, including PKSA-b41t-wbzk-9q51 and PKSA-d5tc-s1qs-h781. CI covers +Laravel 12 and 13 without disabling advisory blocking. Runtime constraints are +unchanged, but this pipeline does not establish Laravel 11 compatibility.