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..9ef56bc --- /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: ['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..0d1e3c2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# 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 12 and 13. + +For one Laravel version, select its matching Testbench before installing: + +```sh +composer require --dev 'orchestra/testbench:^10.0' --no-update +COMPOSER_ROOT_VERSION=0.2.x-dev composer update +composer test +``` + +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 +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 832d082..162b958 100644 --- a/composer.json +++ b/composer.json @@ -38,5 +38,36 @@ "docs": "https://native.watch/docs", "issues": "https://github.com/zinxan/native.desktop/issues", "source": "https://github.com/zinxan/native.desktop" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/pint": "^1.27", + "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" + }, + "autoload-dev": { + "psr-4": { + "ZXN\\NativeWatch\\Desktop\\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..60aaea3 --- /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, + 5 Pest tests and 10 assertions passed. +- `git diff --check`: passed. + +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 +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 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. 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/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..5b88978 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,15 @@ +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; +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..5f9d3e8 --- /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, + ]); + } +}