Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -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.
65 changes: 65 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/vendor/
/composer.lock
/.phpunit.cache/
/.phpunit.result.cache
/.phpstan-cache/
/.php-cs-fixer.cache
74 changes: 51 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
# 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 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.
32 changes: 32 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "^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"
}
}
37 changes: 37 additions & 0 deletions docs/review/sdk-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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 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.

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.
10 changes: 10 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
includes:
- vendor/larastan/larastan/extension.neon

parameters:
level: 7
paths:
- src
tmpDir: .phpstan-cache
dynamicConstantNames:
- FILTER_FLAG_GLOBAL_RANGE
23 changes: 23 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="SDK">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include><directory>src</directory></include>
</source>
<php>
<env name="APP_ENV" value="testing" force="true"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_STORE" value="array"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
</phpunit>
58 changes: 58 additions & 0 deletions pint.json
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 0 additions & 3 deletions src/Laravel/Commands/ReleaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/Laravel/Commands/SymbolsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]);
Binary file added tests/Fixtures/Symbols/CrashFixture.dwarf
Binary file not shown.
Loading
Loading