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
2 changes: 1 addition & 1 deletion .github/workflows/dependency-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
permissions:
contents: read
issues: write
uses: sympress/workflows/.github/workflows/dependency-canary.yml@78d083a9a0176cf0252318b0c69fc64938a727d6
uses: sympress/workflows/.github/workflows/dependency-canary.yml@cd809f3986df3ea617d2d26bdf21ab69cae19c1e
with:
php_version: '8.5'
4 changes: 2 additions & 2 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
- name: Check documentation and public references
run: python3 tools/check-docs.py
workflows:
uses: sympress/workflows/.github/workflows/lint-workflows.yml@78d083a9a0176cf0252318b0c69fc64938a727d6
uses: sympress/workflows/.github/workflows/lint-workflows.yml@cd809f3986df3ea617d2d26bdf21ab69cae19c1e
qa:
uses: sympress/workflows/.github/workflows/sympress-qa.yml@78d083a9a0176cf0252318b0c69fc64938a727d6
uses: sympress/workflows/.github/workflows/sympress-qa.yml@cd809f3986df3ea617d2d26bdf21ab69cae19c1e
with:
php_version: '8.5'
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

## Unreleased
## 1.2.2 — 2026-10-03

- Align read-only Doctor and explicit cache maintenance with Kernel 1.1.4's selected private fallback, configured cache/build roots and discovery metadata.
- Fail diagnostics for unsafe explicit cache directories; report implicit fallback selection without creating directories or executing cache metadata.
- Mark unused cache/build diagnostics and permissions as not-applicable when the optional Kernel is absent, including WordPress core installed in the project root.
- Support explicitly configured external cache/build roots with mode 0700 and matching ownership; reject symlinks, parent traversal and unsafe permissions before any deletion.
- Require Kernel 1.1.4 or newer when the optional kernel is installed, and pin shared QA workflows to the released 1.2.2 source.

## 1.2.1 — 2026-10-03

- Remove persisted process-value fingerprints; evaluate compiled interpolation templates on every warm request without reparsing Dotenv, including rotated, absent and newly supplied values.
- Add side-effect-free `EnvironmentInspection` for requested raw values, sharing Doctor's configured cache/dump/file precedence and rejecting executable PHP or shell substitutions.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@
},
"require-dev": {
"phpunit/phpunit": "^13.0",
"sympress/kernel": "^1.1.1",
"sympress/kernel": "^1.1.4",
"sympress/qa": "^0.1.2"
},
"conflict": {
"sympress/kernel": "<1.1.4",
"symfony/event-dispatcher-contracts": "<3.4.2"
},
"autoload": {
Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0015-kernel-operations-and-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Status: implementation decision, 2026-09-29. Implements ADR 0003 and native acce

`kernel-cache` is an explicitly selected maintenance step. It resolves the canonical environment through the same WordPress environment mapping used by generated configuration. Default paths are `var/cache/{environment}/kernel`; APP_CACHE_DIR and APP_BUILD_DIR overrides resolve relative to the project or retain their absolute directory. The actual SiteKernel path API is compared in integration tests. If an active App kernel was explicitly constructed for a different environment, cache operations fail rather than selecting another environment silently.

All targets are checked before deletion. Cache/build directories are cleared without following child symlinks; an ancestor or target resolving outside the project fails before any deletion. A separate default `discovery-packages.php` is removed when cache overrides leave that metadata elsewhere. Other environments and sibling asset caches remain intact. This does not change the twelve upstream default steps or run a kernel inside Composer.
Runtime 1.2.2 uses a pure counterpart of Kernel 1.1.4's location policy, because calling Kernel's resolver can create and log a private fallback. Its implicit selection is `var/cache-private-{effective-user}/{environment}/kernel` when the normal location is public, group/world writable or unavailable. Tests compare selected paths and real compiled warmup with the installed Kernel. Doctor reports fallback selection without creating it and fails unsafe explicit roots.

All targets are checked before deletion. Cache/build directories are cleared without following child symlinks. In-project targets retain the generic project boundary. Explicit external cache/build roots have a separate boundary: the root must already exist with mode 0700, its owner must match the current PHP identity, and root/ancestor symlinks, parent traversal, foreign descendant owners and group/world writable directories fail. A safe default `discovery-packages.php` is removed when overrides leave legacy metadata there; an unsafe implicit generation that caused fallback is never adopted or deleted. Other environments and sibling asset caches remain intact. This does not change the twelve upstream default steps or run a kernel inside Composer.

Build IDs remain unchanged unless `kernel-build-id` is configured or `--generate-build-id` is used with explicit `kernel-cache` selection. IDs allow 1-128 letters, digits, dots, underscores and hyphens. They are stored atomically in a private `var/runtime/{environment}/kernel-build-id.json`. Generated configuration exposes the ID to the kernel's constant-based reader before boot. Precedence is an existing constant, environment value, configured fallback, persisted ID. Invalid persisted content fails without echoing its value.

Expand All @@ -28,4 +30,4 @@ Fresh environment values, runtime caches and build dumps all reach `%env()%` thr

doctor/check perform read-only checks of entry paths, content directories, symlink boundaries, required database environment names, database status, kernel cache/build paths, boot ownership and permissions. They do not execute setup autoload files, steps or PHP providers. Database status flags are labeled as provided when used. Configuration-disabled or unavailable checks remain unknown; missing kernel support is not-applicable. Exit codes are 0 for passing applicable checks, 1 for failures and 2 for unknown results without failures. JSON and text output contain names and fixed diagnostics, not credential values; configuration/inspection exceptions are redacted.

Readonly production permissions are accepted when the relevant artifact is readable and a valid environment dump is available. A readable directory check is not proof of a complete warm kernel; the separate compiled-kernel smoke provides that evidence. Fresh DDEV consumer/admin/WP-CLI acceptance and Windows execution remain final delivery gates.
Readonly production permissions are accepted when the relevant artifact is readable and a valid environment dump is available. External cache roots retain mode 0700. Doctor observes the calling UID and filesystem namespace; it cannot prove the PHP-FPM identity or a different service namespace. A readable directory check is not proof of a complete warm kernel; the separate compiled-kernel smoke provides that evidence. Fresh DDEV consumer/admin/WP-CLI acceptance and Windows execution remain final delivery gates.
23 changes: 22 additions & 1 deletion docs/kernel-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,28 @@ Composer exposes `sympress-runtime:doctor`, `sympress-runtime:check`, `sympress-

Doctor exits with 0 when applicable checks pass, 1 on a failure, or 2 when a check remains unknown. `--json` includes each check's ID, status and redacted detail. Database-disabled status is unknown; it is never reported as a successful connection. A missing optional kernel is not-applicable. No setup autoload file or custom step executes during diagnosis.

`kernel-cache` preserves other environments and asset directories. It honors APP_CACHE_DIR and APP_BUILD_DIR and removes separately located discovery metadata. Unsafe or escaping symlink paths fail before any clearing. Align explicit SiteKernel environment overrides before running maintenance.
`kernel-cache` preserves other environments and asset directories. It honors APP_CACHE_DIR and APP_BUILD_DIR and clears discovery metadata in the selected cache, including Kernel 1.1.4's private fallback. A safe legacy default discovery file is also retired after an override. The unsafe old generation that triggered fallback stays untouched. Align explicit SiteKernel environment overrides before running maintenance.

Explicit cache/build roots outside the project must already exist, belong to the
user running PHP and have mode 0700. Runtime rejects root or ancestor symlinks,
parent traversal, foreign directory owners and group/world writable directories
before deleting any selected target. External ancestors must belong to that user
or the system root user; shared writable ancestors require the sticky bit, as on
the system temporary directory. It removes only the selected environment's
`kernel` directories, preserving sibling caches and other environments. These
rules apply only to kernel maintenance; generated project artifacts retain their
project boundary.

Doctor computes the same cache location without booting Kernel, creating a
fallback or changing permissions. A missing selected directory remains unknown;
an unsafe explicit root fails. `kernel.cache-migration` warns when Kernel would
select its private per-user fallback. Warmup must run as the PHP-FPM identity.
The guard also treats the known WordPress core/content paths and conventional
`public` directory as public without booting WordPress. Supply `doctor --webroot`
for a different server document root; diagnostics cannot discover web server
configuration from PHP files.
Doctor observes the calling identity; a CLI check cannot prove the web process's
UID or its filesystem namespace. See the [1.2.2 upgrade notes](releases/1.2.2.md).

This is an explicit command-only step: Composer update does not automatically run it. A deployment requiring a deliberate clear or new build ID must invoke it and warm the selected environment before traffic switches.

Expand Down
2 changes: 1 addition & 1 deletion docs/maintainers/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Native default order is the twelve-step sequence in the work order. Actual upstr
| --- | --- | --- | --- | --- | --- | --- |
| doctor/check | W; starter/bin/console | Check paths/env completeness/write permissions/symlinks/DB/kernel cache; JSON or text results with secrets redacted; explicit unknown/unavailable vs pass. | Console/DoctorCommand | `PAR-SYM-001` | verified | Parity |
| validate | W; kernel schema style | Draft 2020-12 root/extra/extension schemas, validation every run, standalone command; no writes or execution of PHP providers during static validation. | Console/ValidateCommand | `PAR-SYM-002` | verified | D01 |
| Kernel cache | W; kernel KernelConfigurationResolver | Canonical env default var/cache/env/kernel, honor APP_CACHE_DIR/build overrides and discovery file; do not follow external symlinks; explicit ID generation. | Steps/KernelCacheStep | `PAR-SYM-003` | verified | ADR 0003 |
| Kernel cache | W; kernel KernelConfigurationResolver | Canonical env and Kernel 1.1.4 private fallback; honor private APP_CACHE_DIR/build overrides including explicit external roots; discovery follows selected cache, no symlinks/foreign owners/traversal; explicit ID generation. | Steps/KernelCacheStep; KernelCacheLocationTest | `PAR-SYM-003` | verified | ADR 0003; ADR 0015 |
| Boot ownership | W; consumers app-starter.php; kernel App | Installed boot-providing package suppresses generated boot; opt-in boot when absent; detect known existing entry and metadata; ambiguous ownership diagnosed; exactly one boot in copy and symlink fixtures. | Steps/KernelBootStep | `PAR-SYM-004` | verified | Parity |
| Kernel console integration | W; kernel Console and bundle discovery | When installed, expose doctor/validate/dump-env via wp console using optional bundle and native command services; no hard kernel dependency for plain WordPress. | Bridge/Kernel/RuntimeBundle | `PAR-SYM-005` | verified | Parity |
| Env container visibility | W; kernel EnvConfig/EnvironmentParameterLoader | Fresh files/cache/dump all hydrate safe ENV/SERVER before kernel boot; %env()% works; build ID also bridged to constant/getenv consumer. | Bridge/Kernel/Environment | `PAR-SYM-006` | verified | D22 |
Expand Down
39 changes: 39 additions & 0 deletions docs/releases/1.2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Runtime 1.2.2 upgrade

Runtime's Doctor and `kernel-cache` now select the same generation as Kernel
1.1.4, including its durable private per-user fallback. When the optional Kernel
is installed, Composer requires 1.1.4 or newer. Plain WordPress installations
remain supported without Kernel.
Doctor marks unused kernel cache/build directories and their permissions as
not-applicable when Kernel is absent, including plain WordPress installed directly
in the project root. Unused cache overrides do not cause diagnostic failures.

An unsafe explicit `APP_CACHE_DIR` fails instead of producing a passing diagnostic
or silently selecting another directory. Implicit fallback selection appears as
`kernel.cache-migration`; Doctor does not create or migrate anything. A missing
selected cache remains unknown until it is warmed. Previously unsafe implicit
generations remain untouched and must not be reused.

For an external deployment cache, provision an absolute `APP_CACHE_DIR` and
optional `APP_BUILD_DIR` outside the webroot. Each root must already exist with
mode 0700 and belong to the PHP-FPM user. Run diagnostics, explicit maintenance
and warmup as that same identity. Symlinks in roots or ancestors, parent traversal,
foreign directory owners and group/world writable directories are rejected before
any deletion. External ancestors must be protected, belong to the calling user or
the system root user, and have the sticky bit if shared writable. Only the selected
environment's `kernel` directory is cleared;
other environments and sibling asset caches are preserved.

Standalone checks also reject known WordPress core/content and conventional
`public` cache locations without loading WordPress. Declare a custom server root
with `doctor --webroot`; Runtime does not inspect the web server configuration.

Doctor checks the calling identity and filesystem namespace. Its readable cache
check does not prove complete warmup or access from a different PHP-FPM UID or
service namespace. Validate the deployed web process separately before traffic
switches. Generic Runtime artifact writes remain confined to the release project.

Validation includes a real Kernel 1.1.4 compiled container, safe external cache and
build roots, fallback warning and read-only diagnostics, unsafe-root failure before
deletion, and preservation of other environments and assets. Hosted release and
consumer acceptance are separate checks.
31 changes: 20 additions & 11 deletions src/Console/Doctor.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function inspect(bool $production = false, bool $databaseHealth = false,
throw new RuntimeException('Invalid compatibility profile.');
}
[$env] = EnvironmentInspection::loadReader($this->config, $this->paths);
$kernel = new KernelPaths($env, $this->paths);
$kernel = new KernelPaths($env, $this->paths, $webroot);
$canonical = $kernel->environment();
if ($production || in_array($canonical, ['staging', 'production'], true)) {
$home = $env->rawValue('WP_HOME');
Expand Down Expand Up @@ -104,17 +104,26 @@ public function inspect(bool $production = false, bool $databaseHealth = false,
}
$packages = new PackageFinder($this->context);
$hasKernel = $packages->findByName('sympress/kernel') !== null;
if (!$hasKernel) {
$record('kernel.cache', 'not-applicable', 'sympress/kernel is not installed.');
$kernelDirectories = [];
foreach (['cache', 'build'] as $id) {
if (!$hasKernel) {
$record('kernel.' . $id, 'not-applicable', 'sympress/kernel is not installed.');
$record('permissions.kernel-' . $id, 'not-applicable', 'sympress/kernel is not installed; its directories are unused.');
continue;
}
try {
$path = $id === 'cache' ? $kernel->cache() : $kernel->build();
$kernel->assertSafe($path, $id);
$kernelDirectories['kernel-' . $id] = $path;
$record('kernel.' . $id, is_dir($path) && is_readable($path) ? 'pass' : 'unknown', is_dir($path) ? 'Selected kernel directory is readable.' : 'Selected kernel directory has not been created; run cache warmup as the PHP-FPM user.');
} catch (Throwable) {
$record('kernel.' . $id, 'fail', 'Unsafe kernel directory. Configure APP_CACHE_DIR/APP_BUILD_DIR outside the webroot without symlinks or group/world write access. External roots require mode 0700. Create and warm them as the PHP-FPM user.');
$record('permissions.kernel-' . $id, 'fail', 'Unsafe kernel directory cannot be used for cache operations.');
}
}
if ($hasKernel) {
foreach (['cache' => $kernel->cache(), 'build' => $kernel->build()] as $id => $path) {
try {
$boundary->assertWritablePath($path);
$record('kernel.' . $id, is_dir($path) && is_readable($path) ? 'pass' : 'unknown', is_dir($path) ? 'Kernel directory is readable.' : 'Kernel directory has not been created.');
} catch (Throwable) {
$record('kernel.' . $id, 'fail', 'Kernel directory is unsafe for runtime cache operations.');
}
if (isset($kernelDirectories['kernel-cache']) && $kernel->usesFallback()) {
$record('kernel.cache-migration', 'warning', 'Kernel selected its private per-user fallback because the implicit cache is unsafe, public or unavailable. Configure a durable APP_CACHE_DIR shared by CLI and PHP-FPM and warm it as the PHP-FPM user. Diagnostics do not create or migrate directories.');
}
try {
$owners = (new BootOwnership($packages, new MuPluginList($packages, $this->paths), $this->config, $this->paths))->providers();
Expand All @@ -124,7 +133,7 @@ public function inspect(bool $production = false, bool $databaseHealth = false,
$record('kernel.boot', 'fail', 'Kernel boot ownership is ambiguous.');
}
}
foreach (['configuration' => $this->paths->root('wp-config.php'), 'content' => $this->paths->wpContent(), 'kernel-cache' => $kernel->cache(), 'kernel-build' => $kernel->build()] as $id => $path) {
foreach (['configuration' => $this->paths->root('wp-config.php'), 'content' => $this->paths->wpContent(), ...$kernelDirectories] as $id => $path) {
$ancestor = $path;
while (!file_exists($ancestor) && dirname($ancestor) !== $ancestor) {
$ancestor = dirname($ancestor);
Expand Down
Loading
Loading