From e405b04b7bebe8066dce020339427a0a8d5936a9 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 | 55 ++++++++++++++++++++++++ composer.json | 31 ++++++++++++++ docs/review/sdk-ci.md | 25 +++++++++++ phpstan.neon | 14 +++++++ phpunit.xml | 23 ++++++++++ pint.json | 58 +++++++++++++++++++++++++ tests/NativeUiCompatibilityTest.php | 9 ++-- tests/Pest.php | 55 ++++++++++++++++++++++++ tests/TestCase.php | 26 ++++++++++++ 14 files changed, 390 insertions(+), 5 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 CONTRIBUTING.md 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/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 new file mode 100644 index 0000000..b0c83e6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +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`. 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. + +## Run the checks + +```sh +COMPOSER_ROOT_VERSION=0.2.x-dev composer install +composer test +``` + +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 5969e82..7211217 100644 --- a/composer.json +++ b/composer.json @@ -43,5 +43,36 @@ "docs": "https://native.watch/docs/install-mobile", "issues": "https://github.com/zinxan/native.mobile/issues", "source": "https://github.com/zinxan/native.mobile" + }, + "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" + }, + "autoload-dev": { + "psr-4": { + "ZXN\\NativeWatch\\Mobile\\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..0ec2bcc --- /dev/null +++ b/docs/review/sdk-ci.md @@ -0,0 +1,25 @@ +# 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, + 40 Pest tests and 115 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 passed 39 tests and failed the subprocess compatibility +check because it loaded the private server bootstrap. It now starts a fresh +Testbench application in the subprocess and still checks that no NativePHP +runtime is installed. All 40 existing tests execute, without skips. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d621d98 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,14 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + level: 7 + paths: + - src + - tests/Fixtures/NativeUiTypeCheck.php + tmpDir: .phpstan-cache + dynamicConstantNames: + - FILTER_FLAG_GLOBAL_RANGE + scanFiles: + - stubs/nativephp.php + - stubs/native-ui.stub 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/tests/NativeUiCompatibilityTest.php b/tests/NativeUiCompatibilityTest.php index 225e4f3..57f97a7 100644 --- a/tests/NativeUiCompatibilityTest.php +++ b/tests/NativeUiCompatibilityTest.php @@ -19,15 +19,15 @@ } }, true, true); if (class_exists('Native\\Mobile\\Runtime') || class_exists('Native\\Mobile\\Edge\\NativeComponent')) exit(1); -$app = require $argv[3]; +$app = Orchestra\Testbench\Foundation\Application::create(); $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); $app->make('config')->set([ 'nativewatch.enabled' => true, 'nativewatch.key' => 'nw_compatibility_test', 'nativewatch.url' => 'https://zxguard.test', 'nativewatch.require_consent' => true, - 'nativewatch.consent.path' => $argv[4].'/missing-consent.json', - 'nativewatch.outbox.path' => $argv[4].'/outbox', + 'nativewatch.consent.path' => $argv[3].'/missing-consent.json', + 'nativewatch.outbox.path' => $argv[3].'/outbox', ]); $app->register(ZXN\NativeWatch\Mobile\MobileServiceProvider::class); if (! trait_exists(ZXN\NativeWatch\Mobile\Concerns\ReportsNativeUiErrors::class)) exit(2); @@ -36,9 +36,8 @@ $process = new Process([ PHP_BINARY, '-r', $script, - base_path('vendor/autoload.php'), + dirname(__DIR__).'/vendor/autoload.php', dirname(__DIR__).'/src', - base_path('bootstrap/app.php'), temporaryOutbox(), ], base_path(), ['JUMP_BRIDGE_PORT' => false]); $process->mustRun(); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..a2b50f7 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,55 @@ +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; +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..b266f0f --- /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, + ]); + } +} From dfa5d59617d18828be3464b172f4ab70774c377c 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 7211217..dfe3156 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,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 0ec2bcc..05eccee 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. 40 Pest tests and 115 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 @@ -23,3 +23,8 @@ The first standalone run passed 39 tests and failed the subprocess compatibility check because it loaded the private server bootstrap. It now starts a fresh Testbench application in the subprocess and still checks that no NativePHP runtime is installed. All 40 existing tests execute, without skips. + +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.