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
10 changes: 8 additions & 2 deletions .claude/pr-review-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ If there are conflicts, resolve them. If the rebase is clean, proceed.
## 4. Run the test suite and lint

```bash
composer ci # everything the CI gate runs
composer test # full suite (excludes integration-destructive)
./vendor/bin/pest packages/<name>/tests # package-specific tests
./vendor/bin/phpcs --standard=phpcs.xml packages/<name>/ # phpcs
./vendor/bin/php-cs-fixer fix packages/<name>/ --dry-run # cs-fixer
composer phpstan # static analysis
```

All tests must pass and lint must be clean. Fix ALL lint errors in touched files before merging, including pre-existing ones.
All tests must pass, lint must be clean, and **PHPStan must report zero errors**. Fix ALL lint errors in touched files before merging, including pre-existing ones.

**No PR CI workflow exists in this repo.** `gh pr checks <N>` will report no checks. The merge-readiness signal is local lint + tests passing plus `gh pr view <N> --json mergeStateStatus,mergeable` returning `CLEAN`/`MERGEABLE`. There is no async CI to wait for — do not write "will merge once CI is green" in PR comments.
**PHPStan is not optional and is not covered by `composer test`.** It went unrun for months precisely because this checklist omitted it, and `packages/core` accumulated 12 errors through normal PR review — type-level drift that PHP tolerates at runtime, so no test could have caught it. `composer ci` runs the whole gate in one command; prefer it.

**The `CI` workflow gates every PR.** `gh pr checks <N>` reports `Tests`, `Lint`, and `Static analysis`. A red check blocks the merge — `develop` is never allowed to carry a failing test, lint error, or PHPStan error. The `integration-destructive` group is excluded from the gate (it deletes `vendor/`, runs `composer update`, and re-runs the suite in a subprocess); it runs on the `Nightly` schedule and again inside `bin/release.sh` before any tag is cut.

## 5. Review the code

Expand Down Expand Up @@ -241,6 +245,8 @@ Before merging any package PR:
- [ ] `composer test` passes
- [ ] `./vendor/bin/phpcs` clean on touched files
- [ ] `./vendor/bin/php-cs-fixer fix --dry-run` clean on touched files
- [ ] `composer phpstan` reports zero errors
- [ ] `gh pr checks <N>` green — `Tests`, `Lint`, `Static analysis` all passing
- [ ] New code has corresponding tests
- [ ] No hardcoded paths or environment-specific values
- [ ] No `final` classes (including exceptions)
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

on:
pull_request:
push:
branches:
- develop
- main

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
coverage: none
ini-values: memory_limit=2G

- name: Install dependencies
uses: ramsey/composer-install@v3

# Excludes integration-destructive: that group deletes vendor/ and composer.lock,
# runs `composer update`, and re-runs the whole suite in a subprocess. It verifies
# install integrity rather than application behaviour, so it runs on the nightly
# schedule below and is enforced again by bin/release.sh before any tag is cut.
- name: Run test suite
run: composer test

lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
coverage: none

- name: Install dependencies
uses: ramsey/composer-install@v3

- name: PHP_CodeSniffer
run: vendor/bin/phpcs --standard=phpcs.xml

- name: PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff

static:
name: Static analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
coverage: none
ini-values: memory_limit=2G

- name: Install dependencies
uses: ramsey/composer-install@v3

- name: PHPStan
run: composer phpstan
31 changes: 31 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Nightly

# The integration-destructive group deletes vendor/ and composer.lock, runs a full
# `composer update`, and re-runs the suite in a subprocess. That is too slow and too
# stateful for a per-PR gate, but it must not go unrun between releases — bin/release.sh
# is otherwise the first thing to discover a broken install, at the worst moment.
on:
schedule:
- cron: '0 9 * * *'
workflow_dispatch:

jobs:
destructive:
name: Full suite including destructive integration
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
coverage: none
ini-values: memory_limit=2G

- name: Install dependencies
uses: ramsey/composer-install@v3

- name: Run full test suite
run: composer test:all
7 changes: 7 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
])
->exclude('vendor')
->exclude('.phpunit.cache')
// Fixtures that are invalid PHP on purpose. The config and codeindexer packages test how
// they behave when handed an unparseable file, so these cannot be made valid — that is the
// test case. php-cs-fixer lints before fixing and exits non-zero on them even when it has
// nothing to change, which would fail the Lint job on a permanently red herring.
->notPath('config/tests/Unit/fixtures/syntax-error.php')
->notPath('codeindexer/tests/Fixtures/AttributeFixtures/src/Broken/SyntaxError.php')
->notPath('codeindexer/tests/Fixtures/ConfigFixtures/module-d/config/broken.php')
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ composer test:all

# Lint (fix)
./vendor/bin/php-cs-fixer fix

# Static analysis — NOT part of composer test; must be zero errors
composer phpstan

# Everything the CI gate runs (tests + lint + static analysis)
composer ci
```

Every PR is gated by the `CI` workflow on `Tests`, `Lint`, and `Static analysis`. A red check blocks the merge; `develop` never carries a failing test, lint error, or PHPStan error.

## Key Conventions

- **Branch naming** - all branches use `feature/{name}` format, regardless of change type (fix, feature, refactor, docs, etc.). Never use `fix/`, `chore/`, or other prefixes
Expand Down
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,13 @@
"php-cs-fixer fix --config=.php-cs-fixer.php",
"phpcbf --standard=phpcs.xml || true"
],
"phpstan": "php -d memory_limit=2G vendor/bin/phpstan analyse",
"ci": [
"@test",
"@cs:check",
"php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff",
"@phpstan"
],
"rector": "rector process"
},
"config": {
Expand Down
10 changes: 5 additions & 5 deletions packages/admin-panel-latte/tests/ResolverIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
'ModuleTemplateResolver resolves admin-panel::dashboard/index to the new admin-panel-latte path',
function (): void {
$adminPanelLatteDir = dirname(__DIR__);

$modules = [
new ModuleManifest(
name: 'marko/admin-panel-latte',
Expand All @@ -22,14 +22,14 @@ function (): void {
extra: ['marko' => ['templates_for' => 'marko/admin-panel']],
),
];

$resolver = new ModuleTemplateResolver(
new ModuleRepository($modules),
new ViewConfig(new FakeConfigRepository(['view.extension' => '.latte'])),
);

$result = $resolver->resolve('admin-panel::dashboard/index');

expect($result)->toBe($adminPanelLatteDir . '/resources/views/dashboard/index.latte');
}
},
);
2 changes: 1 addition & 1 deletion packages/admin-panel-latte/tests/TemplateMigrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
$oldTestPath = dirname(__DIR__, 2) . '/admin-panel/tests/Unit/Template/LayoutTemplateTest.php';

expect(file_exists($oldTestPath))->toBeFalse(
'LayoutTemplateTest.php should not exist in admin-panel/tests/Unit/Template/'
'LayoutTemplateTest.php should not exist in admin-panel/tests/Unit/Template/',
);
});

Expand Down
32 changes: 16 additions & 16 deletions packages/admin-panel-twig/tests/TemplateExistenceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,47 @@
'auth/login.twig exists and renders a form with email and password fields',
function () use ($viewsDir): void {
$path = $viewsDir . '/auth/login.twig';

expect(file_exists($path))->toBeTrue();

$contents = file_get_contents($path);

expect($contents)
->toContain('<form')
->and($contents)->toContain('name="email"')
->and($contents)->toContain('name="password"');
}
},
);

test(
'layout/base.twig exists and contains HTML doctype, sidebar include, and content block',
function () use ($viewsDir): void {
$path = $viewsDir . '/layout/base.twig';

expect(file_exists($path))->toBeTrue();

$contents = file_get_contents($path);

expect($contents)
->toContain('<!DOCTYPE html>')
->and($contents)->toContain("{% include 'admin-panel::partials/sidebar'")
->and($contents)->toContain('{% block content %}');
}
},
);

test(
'dashboard/index.twig exists and extends layout/base.twig with a content block',
function () use ($viewsDir): void {
$path = $viewsDir . '/dashboard/index.twig';

expect(file_exists($path))->toBeTrue();

$contents = file_get_contents($path);

expect($contents)
->toContain("{% extends 'admin-panel::layout/base' %}")
->and($contents)->toContain('{% block content %}');
}
},
);

test('partials/sidebar.twig exists and iterates menu items', function () use ($viewsDir): void {
Expand Down Expand Up @@ -95,16 +95,16 @@ function () use ($viewsDir): void {
function () use ($viewsDir): void {
$layoutPath = $viewsDir . '/layout/base.twig';
$dashboardPath = $viewsDir . '/dashboard/index.twig';

expect(file_exists($layoutPath))->toBeTrue()
->and(file_exists($dashboardPath))->toBeTrue();

$layoutContents = file_get_contents($layoutPath);
$dashboardContents = file_get_contents($dashboardPath);

expect($layoutContents)->toContain('{% block content %}')
->and($dashboardContents)->toContain('{% block content %}')
->and($dashboardContents)->toContain('{% endblock %}');
}
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function getMenuItems(): array

it('requires authentication via AdminAuthMiddleware for dashboard', function (): void {
$middlewareAttributes = (new ReflectionMethod(DashboardController::class, 'index'))->getAttributes(
Middleware::class
Middleware::class,
);

expect($middlewareAttributes)->toHaveCount(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
$viewsDir = dirname(__DIR__, 2) . '/resources/views';

expect(is_dir($viewsDir))->toBeFalse(
'resources/views/ directory should not exist after engine sibling extraction'
'resources/views/ directory should not exist after engine sibling extraction',
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
function (): void {
$reflection = new ReflectionClass(NoDriverException::class);
$constants = $reflection->getConstants();

expect($constants)->toHaveKey('DRIVER_PACKAGES')
->and($constants['DRIVER_PACKAGES'])->toBe([
'marko/admin-api',
'marko/admin-auth',
'marko/admin-panel',
]);
}
},
);

it('provides suggestion with composer require commands for all driver packages', function (): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'skeleton suggest block contains all authentication drivers',
function () use ($knownDriversPath, $skeletonComposerPath) {
KnownDriversValidator::assertSkeletonSuggestContainsAll($knownDriversPath, $skeletonComposerPath);
}
},
);

test('every authentication driver follows marko slash prefix pattern', function () use ($knownDriversPath) {
Expand Down
Loading
Loading