From 1f5d113d9ef5a59c8c5328faf64adcc860dce03f Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:13:28 +0100 Subject: [PATCH 01/15] Update packages Composer moves roave/security-advisories to its latest advisory list, and npm moves electron-to-chromium to 1.5.471, sass to 1.107.1 and tslib to 2.9.1, with nothing moving down. The release-tools and block.json locks need nothing. zizmor stays at 1.31.1, since 1.32.0 needs zizmor-action v0.8.1, which Dependabot's cooldown still holds. Assisted-by: Claude Code:claude-opus-5-5 --- composer.lock | 11 ++++++----- package-lock.json | 18 +++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/composer.lock b/composer.lock index 64cca08..09161a6 100644 --- a/composer.lock +++ b/composer.lock @@ -1070,12 +1070,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "7252ec7fc650313ca2c1ecc1e556da38d93acd56" + "reference": "a09945ae024a4cb2be97d080558455b979e3f743" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7252ec7fc650313ca2c1ecc1e556da38d93acd56", - "reference": "7252ec7fc650313ca2c1ecc1e556da38d93acd56", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a09945ae024a4cb2be97d080558455b979e3f743", + "reference": "a09945ae024a4cb2be97d080558455b979e3f743", "shasum": "" }, "conflict": { @@ -1720,6 +1720,7 @@ "pagarme/pagarme-php": "<3", "pagekit/pagekit": "<=1.0.18", "paragonie/ecc": "<2.0.1", + "paragonie/halite": ">=5,<5.1.5", "paragonie/random_compat": "<2", "paragonie/sodium_compat": "<1.24.1|>=2,<2.5.1", "passbolt/passbolt_api": "<4.6.2", @@ -2236,8 +2237,8 @@ "type": "tidelift" } ], - "time": "2026-11-05T19:32:52+00:00", - "published-time": "2026-11-05T19:32:58+00:00" + "time": "2026-11-07T01:38:18+00:00", + "published-time": "2026-11-07T01:38:22+00:00" }, { "name": "sniccowp/php-scoper-wordpress-excludes", diff --git a/package-lock.json b/package-lock.json index 48ad6bf..64d58e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8815,9 +8815,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.469", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.469.tgz", - "integrity": "sha512-HIR230i9MGRHzaB8QRfXePYN23CF5osVvSqrZWQxqmlIMkIrT//wCEiDMM8yK2Il8TTqw39sjbwR4W2/axBFag==", + "version": "1.5.471", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.471.tgz", + "integrity": "sha512-Lo7jyyneHRYUQYM3WZ2rBEei9FcUIwValFPLifTbr4NTpc6oyUB+nN2rrOtbn3CE5pgz930mYxRJeD+glR7d6Q==", "dev": true, "license": "ISC" }, @@ -16390,9 +16390,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.0.tgz", - "integrity": "sha512-+whPilvVNoAD57xvdIPLQT0gXo4RD1lHYw+Uw8zYHlojaeaMDxK14tc0JmEsz/XMns48h3lpypisMP+3E8eHQA==", + "version": "1.107.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.1.tgz", + "integrity": "sha512-aJ1/LvxOOb04YhmiBjzZn3jVwdHKol+OVhoMV6fFl02fF9T3i+5Mg9l/CnzqtM+NX5Jn5o3lAbs/ZJivTCLHfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18326,9 +18326,9 @@ } }, "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", "dev": true, "license": "0BSD" }, From daa288ff6d2c6ca6fa56f782f22aacb6cb75de19 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:16:04 +0100 Subject: [PATCH 02/15] Indent the docs' JSON, XML and NEON examples with tabs The README and the scripts contract showed JSON at two spaces and the PHPCS ruleset and PHPStan examples at four, while the files they stand for, the manifests, the shipped rulesets and the NEON profiles, indent with tabs under the repository's .editorconfig, so a copied example arrived indented against the consumer's own editor config. The 16 blocks now indent with tabs, as the files they show do; the YAML examples keep two spaces. The change is whitespace only. Assisted-by: Claude Code:claude-opus-5-5 --- README.md | 142 +++++++++++++++++++-------------------- docs/scripts-contract.md | 36 +++++----- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index c3689a4..3fd2d36 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ Add the VCS repository and require the package as a development dependency: ```json { - "repositories": [ - { "type": "vcs", "url": "https://github.com/ahegyes/wordpress-configs.git" } - ], - "require-dev": { - "ahegyes/wordpress-configs": "dev-trunk" - } + "repositories": [ + { "type": "vcs", "url": "https://github.com/ahegyes/wordpress-configs.git" } + ], + "require-dev": { + "ahegyes/wordpress-configs": "dev-trunk" + } } ``` @@ -28,14 +28,14 @@ The root `composer.json` also declares `minimum-stability: dev` with `prefer-sta ```json { - "minimum-stability": "dev", - "prefer-stable": true, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "phpstan/extension-installer": true - } - } + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } + } } ``` @@ -43,9 +43,9 @@ The Node baselines resolve through `node_modules`, so a project that uses them a ```json { - "devDependencies": { - "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#" - } + "devDependencies": { + "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#" + } } ``` @@ -67,24 +67,24 @@ Create a `phpcs.dist.xml` that extends the production profile and names the proj ```xml - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ``` @@ -106,12 +106,12 @@ Create a `phpstan.dist.neon`: ```neon includes: - - vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon + - vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon parameters: - paths: - - your-plugin.php - - src + paths: + - your-plugin.php + - src ``` To add a plugin's conventional paths without listing them, include `phpstan.dist.neon.php` as well. It adds `functions-bootstrap.php`, `functions.php`, `uninstall.php`, `src/`, `includes/`, `models/`, `blocks/` and `templates/` when they exist in the directory PHPStan runs from, and scans `vendor-prefixed/` there; the entry file still goes under `paths`. @@ -120,10 +120,10 @@ To analyze code with no WordPress loaded, include `phpstan.base.dist.neon` alone ```neon includes: - - vendor/szepeviktor/phpstan-wordpress/extension.neon - - vendor/johnbillion/wp-compat/extension.neon - - vendor/swissspidy/phpstan-no-private/rules.neon - - vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon + - vendor/szepeviktor/phpstan-wordpress/extension.neon + - vendor/johnbillion/wp-compat/extension.neon + - vendor/swissspidy/phpstan-no-private/rules.neon + - vendor/ahegyes/wordpress-configs/php/quality-assurance/phpstan.dist.neon ``` The package installs `phpstan-deprecation-rules`, `phpstan-strict-rules`, `szepeviktor/phpstan-wordpress` (WordPress signatures and types), `johnbillion/wp-compat` (WordPress version compatibility) and `swissspidy/phpstan-no-private` (private WordPress APIs). @@ -136,9 +136,9 @@ Require php-scoper in the plugin itself, because Composer does not install this ```json { - "require-dev": { - "humbug/php-scoper": "^0.18" - } + "require-dev": { + "humbug/php-scoper": "^0.18" + } } ``` @@ -146,19 +146,19 @@ Declare the scoping prefix, load the generated autoloader, and scope after every ```json { - "autoload": { - "files": [ - "vendor-prefixed/scoper-autoload.php" - ] - }, - "scripts": { - "post-autoload-dump": [ - "DeepWebSolutions\\Config\\Composer\\ScopePhpDependencies::postAutoloadDump" - ] - }, - "extra": { - "scoping-prefix": "YourVendor\\YourPlugin\\Scoped" - } + "autoload": { + "files": [ + "vendor-prefixed/scoper-autoload.php" + ] + }, + "scripts": { + "post-autoload-dump": [ + "DeepWebSolutions\\Config\\Composer\\ScopePhpDependencies::postAutoloadDump" + ] + }, + "extra": { + "scoping-prefix": "YourVendor\\YourPlugin\\Scoped" + } } ``` @@ -194,13 +194,13 @@ The project or a scoped package declares the stubs files of the host symbols the ```json { - "extra": { - "scoping-stubs": [ - "php-stubs/woocommerce-stubs", - "php-stubs/woocommerce-stubs:woocommerce-packages-stubs.php", - "./stubs/host-plugin.php" - ] - } + "extra": { + "scoping-stubs": [ + "php-stubs/woocommerce-stubs", + "php-stubs/woocommerce-stubs:woocommerce-packages-stubs.php", + "./stubs/host-plugin.php" + ] + } } ``` @@ -208,9 +208,9 @@ A package whose strings take the host plugin's text domain declares the domain i ```json { - "extra": { - "scoping-text-domain": "your-library" - } + "extra": { + "scoping-text-domain": "your-library" + } } ``` @@ -235,8 +235,8 @@ TypeScript extends the base by path: ```json { - "extends": "@ahegyes/wordpress-configs/node/tsconfig.base.json", - "include": ["client/**/*"] + "extends": "@ahegyes/wordpress-configs/node/tsconfig.base.json", + "include": ["client/**/*"] } ``` diff --git a/docs/scripts-contract.md b/docs/scripts-contract.md index 7296e88..5624e7b 100644 --- a/docs/scripts-contract.md +++ b/docs/scripts-contract.md @@ -36,12 +36,12 @@ The two shared profiles run as two scripts, one over the project and one over `t ```json { - "scripts": { - "format:php": "phpcbf --standard=./phpcs.dist.xml --basepath=. ./ -v", - "format:php:tests": "phpcbf --standard=./phpcs.tests.dist.xml --basepath=. ./tests -v", - "lint:php:phpcs": "phpcs --standard=./phpcs.dist.xml --basepath=. ./ -v", - "lint:php:phpcs:tests": "phpcs --standard=./phpcs.tests.dist.xml --basepath=. ./tests -v" - } + "scripts": { + "format:php": "phpcbf --standard=./phpcs.dist.xml --basepath=. ./ -v", + "format:php:tests": "phpcbf --standard=./phpcs.tests.dist.xml --basepath=. ./tests -v", + "lint:php:phpcs": "phpcs --standard=./phpcs.dist.xml --basepath=. ./ -v", + "lint:php:phpcs:tests": "phpcs --standard=./phpcs.tests.dist.xml --basepath=. ./tests -v" + } } ``` @@ -58,8 +58,8 @@ Include a floors ruleset after the shared profile. A `phpcs.floors.xml` holds th ```xml - - + + ``` @@ -68,8 +68,8 @@ The project ruleset includes it after the profile: ```xml - - + + ``` @@ -87,9 +87,9 @@ PHPStan finds the project's `phpstan.dist.neon` on its own, so the script needs ```json { - "scripts": { - "lint:php:phpstan": "phpstan analyse -v --memory-limit=1G" - } + "scripts": { + "lint:php:phpstan": "phpstan analyse -v --memory-limit=1G" + } } ``` @@ -115,10 +115,10 @@ A package the plugin ships states the WordPress version, the WooCommerce version ```json { - "extra": { - "requires-wp": "7.1", - "requires-wc": "11.2", - "requires-plugins": ["woocommerce"] - } + "extra": { + "requires-wp": "7.1", + "requires-wc": "11.2", + "requires-plugins": ["woocommerce"] + } } ``` From 66dd09d47bf7b7c0663235219c7fba34baa1f3f8 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:16:38 +0100 Subject: [PATCH 03/15] Find the release, audit and Playwright steps by imperative names The workflows name their steps in two voices: telegraphic forms such as "Assert required files present" and "Redirect wp-env at built artifact" beside sentences such as "Find the main plugin file", and the audit steps by their bare commands. The tests that run those steps by name now look for the imperative names, so they fail until the workflows take them. Assisted-by: Claude Code:claude-opus-5-5 --- tests/Unit/PlaywrightWorkflowTest.php | 4 ++-- tests/Unit/ReleaseWorkflowTest.php | 18 +++++++++--------- tests/Unit/SupplyChainAuditWorkflowTest.php | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Unit/PlaywrightWorkflowTest.php b/tests/Unit/PlaywrightWorkflowTest.php index 095c6ba..8699ed9 100644 --- a/tests/Unit/PlaywrightWorkflowTest.php +++ b/tests/Unit/PlaywrightWorkflowTest.php @@ -11,14 +11,14 @@ final class PlaywrightWorkflowTest extends TestCase { public function test_the_installed_playwright_version_keys_the_browser_cache(): void { $this->write_project( array( 'node_modules/@playwright/test/package.json' => '{"name": "@playwright/test", "version": "1.64.1"}' ) ); - $result = $this->run_step( 'reusable-playwright-e2e.yml', 'e2e', 'Resolve Playwright version', array() ); + $result = $this->run_step( 'reusable-playwright-e2e.yml', 'e2e', 'Resolve the Playwright version', array() ); self::assertSame( 0, $result['status'], $result['output'] ); self::assertSame( array( 'version' => '1.64.1' ), $result['outputs'] ); } public function test_a_project_without_playwright_fails_with_a_named_error(): void { - $result = $this->run_step( 'reusable-playwright-e2e.yml', 'e2e', 'Resolve Playwright version', array() ); + $result = $this->run_step( 'reusable-playwright-e2e.yml', 'e2e', 'Resolve the Playwright version', array() ); self::assertSame( 1, $result['status'], $result['output'] ); self::assertMatchesRegularExpression( '/^::error::@playwright\/test is not installed/m', $result['output'] ); diff --git a/tests/Unit/ReleaseWorkflowTest.php b/tests/Unit/ReleaseWorkflowTest.php index b6110e5..288d7a2 100644 --- a/tests/Unit/ReleaseWorkflowTest.php +++ b/tests/Unit/ReleaseWorkflowTest.php @@ -436,7 +436,7 @@ public static function provide_archive_entries(): array { public function test_archive_must_not_ship_development_files( string $entry, int $expected_status ): void { $this->write_archive( array( 'acme-plugin.php', $entry ) ); - $result = $this->run_build_step( 'Assert forbidden entries absent', array() ); + $result = $this->run_build_step( 'Assert the archive holds no forbidden entries', array() ); self::assertSame( $expected_status, $result['status'], $result['output'] ); } @@ -477,7 +477,7 @@ public function test_the_archive_must_ship_every_scoped_php_file( array $archive ); } - $result = $this->run_build_step( 'Assert scoped packages bundled', array() ); + $result = $this->run_build_step( 'Assert the archive bundles the scoped packages', array() ); self::assertSame( $expected_status, $result['status'], $result['output'] ); if ( '' !== $named ) { @@ -517,7 +517,7 @@ public function test_a_release_needs_a_successful_push_run_of_every_required_wor $result = $this->run_step( 'reusable-release.yml', 'provenance', - 'Verify required workflows passed', + 'Verify the required workflows passed', array( 'GH_TOKEN' => 'token', 'REPOSITORY' => 'acme/acme-plugin', @@ -563,7 +563,7 @@ public static function provide_release_inputs(): array { #[DataProvider( 'provide_release_inputs' )] public function test_a_release_runs_from_a_version_tag_with_a_safe_slug_and_path( string $ref_type, string $ref_name, string $project_path, string $publish, string $slug, int $expected_status, string $expected ): void { $result = $this->run_build_step( - 'Enforce release inputs', + 'Enforce the release inputs', array( 'REF_TYPE' => $ref_type, 'REF_NAME' => $ref_name, @@ -666,7 +666,7 @@ public function test_the_release_publishes_the_build_s_notes( string $version, s } public function test_the_wp_org_deploy_refuses_a_prerelease_tag(): void { - $result = $this->run_step( 'reusable-release.yml', 'wp-org', 'Re-verify version tag before deploy', array( 'REF_NAME' => 'v1.3.0-beta.1' ) ); + $result = $this->run_step( 'reusable-release.yml', 'wp-org', 'Verify the version tag before the deploy', array( 'REF_NAME' => 'v1.3.0-beta.1' ) ); self::assertSame( 1, $result['status'], $result['output'] ); self::assertMatchesRegularExpression( '/^::error::Refusing to deploy/m', $result['output'] ); @@ -680,7 +680,7 @@ public function test_required_files_are_found_in_a_large_archive(): void { $this->write_archive( $files ); - $result = $this->run_build_step( 'Assert required files present', array( 'WP_ORG' => 'false' ) ); + $result = $this->run_build_step( 'Assert the archive holds the required files', array( 'WP_ORG' => 'false' ) ); self::assertSame( 0, $result['status'], $result['output'] ); } @@ -699,7 +699,7 @@ public static function provide_archived_main_files(): array { public function test_the_archive_must_ship_the_main_plugin_file( string $archived, int $expected_status ): void { $this->write_archive( array( $archived ) ); - $result = $this->run_build_step( 'Assert required files present', array( 'WP_ORG' => 'false' ) ); + $result = $this->run_build_step( 'Assert the archive holds the required files', array( 'WP_ORG' => 'false' ) ); self::assertSame( $expected_status, $result['status'], $result['output'] ); } @@ -736,7 +736,7 @@ public function test_a_plugin_that_does_not_require_configs_gets_no_release_tool public function test_the_archive_command_runs_from_the_release_tools(): void { $this->write_executable( 'bin/wp', "#!/usr/bin/env bash\necho \"wp \$*\" >> \"\$HOME/calls\"\n" ); - $result = $this->run_build_step( 'Create plugin archive', array() ); + $result = $this->run_build_step( 'Create the plugin archive', array() ); self::assertSame( 0, $result['status'], $result['output'] ); self::assertSame( array( 'wp dist-archive . ./acme-plugin.zip --plugin-dirname=acme-plugin' ), \file( $this->work_dir . '/calls', \FILE_IGNORE_NEW_LINES ) ); @@ -820,7 +820,7 @@ public function test_the_artifact_redirect_joins_the_config_s_override( string $ $result = $this->run_step( 'reusable-release.yml', 'test', - 'Redirect wp-env at built artifact', + 'Point wp-env at the built artifact', array( 'PLUGIN_SLUG' => 'acme-plugin', 'WP_ENV_CONFIG_FILE' => $config_file, diff --git a/tests/Unit/SupplyChainAuditWorkflowTest.php b/tests/Unit/SupplyChainAuditWorkflowTest.php index 7593fc0..12641eb 100644 --- a/tests/Unit/SupplyChainAuditWorkflowTest.php +++ b/tests/Unit/SupplyChainAuditWorkflowTest.php @@ -85,7 +85,7 @@ public function test_npm_audit_softens_only_a_report_that_holds_findings( string $result = $this->run_step( 'reusable-supply-chain-audit.yml', 'npm-audit', - 'npm audit', + 'Run npm audit', array( 'AUDIT_FLAGS' => '--omit=dev --audit-level=high', 'FAIL_ON_FINDINGS' => $fail_on_findings, @@ -108,7 +108,7 @@ protected function audit( ?string $fixture, string $fail_on_findings ): array { return $this->run_step( 'reusable-supply-chain-audit.yml', 'composer-audit', - 'composer audit', + 'Run composer audit', array( 'AUDIT_FLAGS' => '--abandoned=report', 'FAIL_ON_FINDINGS' => $fail_on_findings, From 4643291bebe3f5311a8e39d9689b2e97026ebd8f Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:21:30 +0100 Subject: [PATCH 04/15] Name every workflow step and job in one imperative form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step names mixed telegraphic forms ("Assert required files present", "Record archive digest") with sentences ("Find the main plugin file"), carried parenthetical narration ("dev — runs the scoping pipeline", "for wp-env"), spelled the verb "Setup", and were left off some setup steps while the same setups elsewhere had them. Job names mixed cases and forms, such as "PHP Lint", "PHP 7.4 syntax" and "Plugin Check fixture build". Every step and job now reads as an imperative in sentence case, with an article where English takes one, so Quality's lint job reads "Lint PHP" as in the other repositories. Every step but the repository checkout is named, a matrix value stays in its parentheses, and the release's development-install note moves into the comment above it. No ruleset requires a check by name. Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/quality.yml | 24 +++--- .../workflows/reusable-block-json-check.yml | 2 +- .github/workflows/reusable-codeql.yml | 6 +- .github/workflows/reusable-php-lint.yml | 17 ++-- .../workflows/reusable-php-syntax-check.yml | 4 +- .github/workflows/reusable-phpunit.yml | 8 +- .github/workflows/reusable-playwright-e2e.yml | 16 ++-- .github/workflows/reusable-plugin-check.yml | 4 +- .github/workflows/reusable-release.yml | 82 +++++++++---------- .../reusable-scripts-styles-lint.yml | 6 +- .../workflows/reusable-supply-chain-audit.yml | 16 ++-- .../workflows/reusable-workflow-checks.yml | 10 +-- .github/workflows/tests.yml | 2 +- 13 files changed, 101 insertions(+), 96 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 465fa14..9ff56e1 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -17,7 +17,7 @@ permissions: {} jobs: lint-php: - name: PHP Lint + name: Lint PHP permissions: contents: read uses: ./.github/workflows/reusable-php-lint.yml @@ -25,13 +25,13 @@ jobs: scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]' php-syntax: - name: PHP Syntax + name: Check PHP syntax permissions: contents: read uses: ./.github/workflows/reusable-php-syntax-check.yml php-syntax-legacy: - name: PHP 7.4 syntax + name: Check PHP 7.4 syntax # The fixture stands in for a consumer's by-path files, which must still parse on PHP 7.4. permissions: contents: read @@ -41,7 +41,7 @@ jobs: paths: '["tests/Fixtures/build-fixture"]' phpcs-smoke: - name: PHPCS dist-file smoke + name: Smoke-test the PHPCS production profile # Self-lint runs the shared profiles only through the root rulesets, which strip the WP-runtime # sniffs consumers get — this job scans a plugin-shaped fixture with the raw production profile. runs-on: ubuntu-latest @@ -53,7 +53,7 @@ jobs: with: persist-credentials: false - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.5' @@ -70,7 +70,7 @@ jobs: - name: Explain php/quality-assurance/phpcs.dist.xml run: vendor/bin/phpcs -e --standard=php/quality-assurance/phpcs.dist.xml - - name: Scan fixture with php/quality-assurance/phpcs.dist.xml + - name: Scan the fixture with php/quality-assurance/phpcs.dist.xml shell: bash run: | # The production profile skips */tests/*, so a temp copy is scanned outside that path. @@ -88,13 +88,13 @@ jobs: scripts: '["lint:scripts", "lint:config"]' supply-chain: - name: Supply-chain audit + name: Audit the supply chain permissions: contents: read uses: ./.github/workflows/reusable-supply-chain-audit.yml supply-chain-release-tools: - name: Supply-chain audit (release tools) + name: Audit the release tools permissions: contents: read uses: ./.github/workflows/reusable-supply-chain-audit.yml @@ -103,7 +103,7 @@ jobs: npm-audit: false supply-chain-block-json-tools: - name: Supply-chain audit (block.json tools) + name: Audit the block.json tools permissions: contents: read uses: ./.github/workflows/reusable-supply-chain-audit.yml @@ -112,7 +112,7 @@ jobs: composer-audit: false block-json: - name: block.json schema check + name: Check block.json files # Dogfoods reusable-block-json-check against a throwaway fixture; this tooling repo ships no real blocks. permissions: contents: read @@ -121,7 +121,7 @@ jobs: project-path: tests/Fixtures/block-json plugin-check-build: - name: Plugin Check fixture build + name: Build the Plugin Check fixture runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -140,7 +140,7 @@ jobs: retention-days: 1 plugin-check: - name: Plugin Check + name: Run Plugin Check needs: plugin-check-build # The fixture fails Plugin Check unless the GitHub profile skips its Update URI and its missing readme.txt and the scan skips vendor-prefixed/. permissions: diff --git a/.github/workflows/reusable-block-json-check.yml b/.github/workflows/reusable-block-json-check.yml index 4741351..d0ff7d5 100644 --- a/.github/workflows/reusable-block-json-check.yml +++ b/.github/workflows/reusable-block-json-check.yml @@ -33,7 +33,7 @@ jobs: sparse-checkout: node/block-json persist-credentials: false - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '26' diff --git a/.github/workflows/reusable-codeql.yml b/.github/workflows/reusable-codeql.yml index d2f5321..31c50da 100644 --- a/.github/workflows/reusable-codeql.yml +++ b/.github/workflows/reusable-codeql.yml @@ -97,10 +97,12 @@ jobs: with: persist-credentials: false - - uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + - name: Initialize CodeQL + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: ${{ matrix.language }} - - uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + - name: Run the CodeQL analysis + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/reusable-php-lint.yml b/.github/workflows/reusable-php-lint.yml index c1ddadc..3016617 100644 --- a/.github/workflows/reusable-php-lint.yml +++ b/.github/workflows/reusable-php-lint.yml @@ -43,7 +43,7 @@ jobs: fi composer-validate: - name: composer validate + name: Validate composer.json runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -52,17 +52,18 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + - name: Set up PHP + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} coverage: none tools: composer:v2 - - name: composer validate --strict + - name: Run composer validate --strict working-directory: ${{ inputs.project-path }} run: composer validate --strict lint: - name: ${{ matrix.script }} + name: Run ${{ matrix.script }} needs: validate-inputs runs-on: ubuntu-latest timeout-minutes: 30 @@ -76,16 +77,18 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + - name: Set up PHP + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} coverage: none tools: composer:v2 - - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 with: working-directory: ${{ inputs.project-path }} composer-options: ${{ inputs.composer-options }} - - name: composer ${{ matrix.script }} + - name: Run composer ${{ matrix.script }} working-directory: ${{ inputs.project-path }} env: SCRIPT: ${{ matrix.script }} diff --git a/.github/workflows/reusable-php-syntax-check.yml b/.github/workflows/reusable-php-syntax-check.yml index a2afc5f..d7ea085 100644 --- a/.github/workflows/reusable-php-syntax-check.yml +++ b/.github/workflows/reusable-php-syntax-check.yml @@ -53,7 +53,7 @@ jobs: fi syntax: - name: Syntax (PHP ${{ matrix.php-version }}) + name: Check syntax (PHP ${{ matrix.php-version }}) needs: validate-inputs runs-on: ubuntu-latest timeout-minutes: 30 @@ -76,7 +76,7 @@ jobs: with: name: ${{ inputs.artifact }} - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} diff --git a/.github/workflows/reusable-phpunit.yml b/.github/workflows/reusable-phpunit.yml index bc6df54..d186775 100644 --- a/.github/workflows/reusable-phpunit.yml +++ b/.github/workflows/reusable-phpunit.yml @@ -58,7 +58,7 @@ permissions: {} jobs: phpunit: - name: PHPUnit (PHP ${{ inputs.php-version }} / WP ${{ inputs.wp-env-core != '' && inputs.wp-env-core || inputs.wp-version != '' && inputs.wp-version || (inputs.needs-wp-env && 'consumer-default' || 'no-wp-env') }}) + name: Run PHPUnit (PHP ${{ inputs.php-version }} / WP ${{ inputs.wp-env-core != '' && inputs.wp-env-core || inputs.wp-version != '' && inputs.wp-version || (inputs.needs-wp-env && 'consumer-default' || 'no-wp-env') }}) runs-on: ubuntu-latest timeout-minutes: 45 permissions: @@ -71,7 +71,7 @@ jobs: with: persist-credentials: false - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} @@ -88,7 +88,7 @@ jobs: exit 1 fi - - name: Setup Node.js (for wp-env) + - name: Set up Node.js if: ${{ inputs.needs-wp-env }} uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: @@ -152,7 +152,7 @@ jobs: fi ./node_modules/.bin/wp-env start "${args[@]}" - - name: Run tests + - name: Run the tests working-directory: ${{ inputs.project-path }} env: COMPOSER_SCRIPT: ${{ inputs.composer-script }} diff --git a/.github/workflows/reusable-playwright-e2e.yml b/.github/workflows/reusable-playwright-e2e.yml index 7dd386d..d1d6f0a 100644 --- a/.github/workflows/reusable-playwright-e2e.yml +++ b/.github/workflows/reusable-playwright-e2e.yml @@ -52,7 +52,7 @@ permissions: {} jobs: e2e: - name: Playwright E2E + name: Run the Playwright suite runs-on: ubuntu-latest timeout-minutes: 45 permissions: @@ -65,7 +65,7 @@ jobs: with: persist-credentials: false - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} @@ -81,7 +81,7 @@ jobs: exit 1 fi - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: ${{ inputs.project-path }}/package.json @@ -110,20 +110,20 @@ jobs: BUILD_SCRIPT: ${{ inputs.build-script }} run: npm run "$BUILD_SCRIPT" - - name: Resolve Playwright version + - name: Resolve the Playwright version id: playwright working-directory: ${{ inputs.project-path }} run: | version=$(node -p "require('@playwright/test/package.json').version") || { echo "::error::@playwright/test is not installed in this project — add it to devDependencies."; exit 1; } echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Cache Playwright browsers + - name: Cache the Playwright browsers uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }} - - name: Install Playwright browsers + - name: Install the Playwright browsers working-directory: ${{ inputs.project-path }} run: npx --no-install playwright install --with-deps chromium @@ -153,13 +153,13 @@ jobs: fi ./node_modules/.bin/wp-env start "${args[@]}" - - name: Run Playwright tests + - name: Run the Playwright tests working-directory: ${{ inputs.project-path }} env: PLAYWRIGHT_SCRIPT: ${{ inputs.playwright-script }} run: npm run "$PLAYWRIGHT_SCRIPT" - - name: Upload Playwright report on failure + - name: Upload the Playwright report on failure if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/.github/workflows/reusable-plugin-check.yml b/.github/workflows/reusable-plugin-check.yml index 3ed17b2..40e7563 100644 --- a/.github/workflows/reusable-plugin-check.yml +++ b/.github/workflows/reusable-plugin-check.yml @@ -31,7 +31,7 @@ permissions: {} jobs: plugin-check: - name: Plugin Check + name: Run Plugin Check runs-on: ubuntu-latest timeout-minutes: 30 permissions: {} @@ -39,7 +39,7 @@ jobs: # The action starts its own wp-env without a phpVersion, and WordPress refuses to activate a plugin whose Requires PHP the default image does not meet. WP_ENV_PHP_VERSION: ${{ inputs.php-version }} steps: - - name: Validate plugin-slug + - name: Validate the plugin-slug input env: PLUGIN_SLUG: ${{ inputs.plugin-slug }} run: | diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index f490551..955079e 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -63,7 +63,7 @@ permissions: {} jobs: build: - name: Build artifact + name: Build the artifact runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -79,7 +79,7 @@ jobs: requires-scoped: ${{ steps.scoping.outputs.requires-scoped }} version: ${{ steps.versions.outputs.version }} steps: - - name: Enforce release inputs + - name: Enforce the release inputs id: release-inputs env: REF_TYPE: ${{ github.ref_type }} @@ -131,7 +131,7 @@ jobs: with: persist-credentials: false - - name: Verify .distignore exists + - name: Verify that .distignore exists working-directory: ${{ inputs.project-path }} run: | if [ ! -f .distignore ]; then @@ -222,7 +222,7 @@ jobs: requires_scoped=$(jq -r 'if (.extra["scoping-prefix"] | type) == "string" then "true" else "false" end' composer.json) echo "requires-scoped=$requires_scoped" >> "$GITHUB_OUTPUT" - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} @@ -238,14 +238,14 @@ jobs: exit 1 fi - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: ${{ inputs.project-path }}/package.json package-manager-cache: false - # A plain install restores no dependency cache, which another workflow could have written and Composer cannot check without dist checksums. - - name: Install Composer dependencies (dev — runs the scoping pipeline) + # A development install runs the scoping pipeline, and this plain one restores no dependency cache, which another workflow could have written and Composer cannot check without dist checksums. + - name: Install the development dependencies working-directory: ${{ inputs.project-path }} run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-req=php+ @@ -264,13 +264,13 @@ jobs: composer install --working-dir="$tools" --no-interaction --no-progress --prefer-dist --ignore-platform-req=php+ echo "$tools/vendor/bin" >> "$GITHUB_PATH" - - name: Build assets + - name: Build the assets working-directory: ${{ inputs.project-path }} run: | npm ci npm run build --if-present - - name: Generate POT + - name: Generate the POT file if: ${{ inputs.generate-pot }} working-directory: ${{ inputs.project-path }} run: | @@ -289,7 +289,7 @@ jobs: fi # --no-scripts avoids invoking handlers that were just pruned from require-dev. - - name: Prune to production dependencies + - name: Prune to the production dependencies working-directory: ${{ inputs.project-path }} run: composer install --no-dev --no-interaction --no-progress --prefer-dist --ignore-platform-req=php+ --optimize-autoloader --no-scripts @@ -410,11 +410,11 @@ jobs: exit( array() === $errors ? 0 : 1 ); PHP - - name: Create plugin archive + - name: Create the plugin archive working-directory: ${{ inputs.project-path }} run: wp dist-archive . "./$PLUGIN_SLUG.zip" --plugin-dirname="$PLUGIN_SLUG" - - name: Assert required files present + - name: Assert the archive holds the required files working-directory: ${{ inputs.project-path }} run: | entries=$(unzip -Z1 "$PLUGIN_SLUG.zip") @@ -431,7 +431,7 @@ jobs: done exit "$status" - - name: Assert scoped packages bundled + - name: Assert the archive bundles the scoped packages if: ${{ steps.scoping.outputs.requires-scoped == 'true' }} working-directory: ${{ inputs.project-path }} run: | @@ -448,7 +448,7 @@ jobs: exit 1 fi - - name: Assert forbidden entries absent + - name: Assert the archive holds no forbidden entries working-directory: ${{ inputs.project-path }} run: | entries=$(unzip -Z1 "$PLUGIN_SLUG.zip") @@ -481,14 +481,14 @@ jobs: done <<< "$entries" exit "$status" - - name: Record archive digest + - name: Record the archive digest id: archive-digest working-directory: ${{ inputs.project-path }} run: | digest=$(sha256sum "$PLUGIN_SLUG.zip" | awk '{print $1}') echo "digest=$digest" >> "$GITHUB_OUTPUT" - - name: Prepare Plugin Check build + - name: Prepare the Plugin Check build if: ${{ inputs.plugin-check }} working-directory: ${{ inputs.project-path }} run: | @@ -507,14 +507,14 @@ jobs: # An empty token skips the pull request comment, which this job's token has no permission to post. repo-token: '' - - name: Upload zip artifact + - name: Upload the zip artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ inputs.plugin-slug }}-zip path: ${{ inputs.project-path }}/${{ inputs.plugin-slug }}.zip retention-days: 30 - - name: Prepare wp.org assets artifact + - name: Prepare the wp.org assets artifact if: ${{ inputs.wp-org }} working-directory: ${{ inputs.project-path }} run: | @@ -525,7 +525,7 @@ jobs: fi touch wp-org-assets-artifact/__artifact_keep__ - - name: Upload wp.org assets artifact + - name: Upload the wp.org assets artifact if: ${{ inputs.wp-org }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: @@ -534,7 +534,7 @@ jobs: retention-days: 30 test: - name: Test built artifact + name: Test the built artifact needs: build runs-on: ubuntu-latest timeout-minutes: 45 @@ -550,13 +550,13 @@ jobs: with: persist-credentials: false - - name: Download built artifact + - name: Download the built artifact uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ inputs.plugin-slug }}-zip path: ./dist - - name: Verify artifact digest + - name: Verify the artifact digest env: EXPECTED_DIGEST: ${{ needs.build.outputs.digest }} run: | @@ -566,27 +566,27 @@ jobs: exit 1 fi - - name: Extract artifact + - name: Extract the artifact run: unzip -q "./dist/$PLUGIN_SLUG.zip" -d ./built-test - - name: Setup PHP + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} coverage: none tools: composer:v2 - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: ${{ inputs.project-path }}/package.json package-manager-cache: false - - name: Install Composer dependencies (E2E harness) + - name: Install Composer dependencies working-directory: ${{ inputs.project-path }} run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-req=php+ - - name: Install npm dependencies (E2E runner) + - name: Install npm dependencies working-directory: ${{ inputs.project-path }} run: | npm ci @@ -595,7 +595,7 @@ jobs: exit 1 fi - - name: Redirect wp-env at built artifact + - name: Point wp-env at the built artifact working-directory: ${{ inputs.project-path }} env: BUILT_PATH: ${{ github.workspace }}/built-test/${{ inputs.plugin-slug }} @@ -663,7 +663,7 @@ jobs: port=$(jq -r -s 'reduce .[] as $file ({}; . * $file) | .env.development.port // .port // 8888' "${files[@]}") curl --fail --silent --show-error --output /dev/null "http://localhost:${port}/" - - name: Run E2E tests against built artifact + - name: Run the E2E tests against the built artifact working-directory: ${{ inputs.project-path }} run: | if [ -d node_modules/@playwright/test ]; then @@ -672,7 +672,7 @@ jobs: npm run test:e2e --if-present provenance: - name: Verify release provenance + name: Verify the release provenance # A dispatched dry run rehearses this check; a pull request's merge commit has no push runs, and a push-triggered dry run would race the runs it looks up. if: ${{ inputs.publish || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest @@ -681,7 +681,7 @@ jobs: actions: read steps: # A release reuses the suites proven at the exact released commit instead of running them again. - - name: Verify required workflows passed + - name: Verify the required workflows passed env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} @@ -715,7 +715,7 @@ jobs: fi release: - name: Publish GitHub release + name: Publish the GitHub release needs: [build, test, provenance] if: ${{ inputs.publish }} runs-on: ubuntu-latest @@ -727,13 +727,13 @@ jobs: env: PLUGIN_SLUG: ${{ inputs.plugin-slug }} steps: - - name: Download built artifact + - name: Download the built artifact uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ inputs.plugin-slug }}-zip path: ./dist - - name: Verify artifact digest + - name: Verify the artifact digest env: EXPECTED_DIGEST: ${{ needs.build.outputs.digest }} run: | @@ -782,19 +782,19 @@ jobs: env: PLUGIN_SLUG: ${{ inputs.plugin-slug }} steps: - - name: Download built artifact + - name: Download the built artifact uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ inputs.plugin-slug }}-zip path: ./dist - - name: Download wp.org assets artifact + - name: Download the wp.org assets artifact uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ inputs.plugin-slug }}-wp-org-assets path: ./wp-org-assets - - name: Verify artifact digest + - name: Verify the artifact digest env: EXPECTED_DIGEST: ${{ needs.build.outputs.digest }} run: | @@ -805,12 +805,12 @@ jobs: fi # The deploy re-checks its own ref, so no change to the build's checks lets a branch or prerelease ref reach SVN. - - name: Re-verify version tag before deploy + - name: Verify the version tag before the deploy env: REF_NAME: ${{ github.ref_name }} run: printf '%s' "$REF_NAME" | grep -Eq '^v(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*)){2}$' || { echo "::error::Refusing to deploy from non-version ref '$REF_NAME'."; exit 1; } - - name: Verify SVN credentials + - name: Verify the SVN credentials env: SVN_USERNAME: ${{ secrets.SVN_USERNAME }} SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} @@ -820,14 +820,14 @@ jobs: exit 1 fi - - name: Extract verified artifact for publish + - name: Extract the verified artifact run: | rm -rf ./publish mkdir -p ./publish unzip -q "./dist/$PLUGIN_SLUG.zip" -d ./publish test -d "./publish/$PLUGIN_SLUG" - - name: Prepare wp.org assets + - name: Prepare the wp.org assets run: | find ./wp-org-assets -type f -name __artifact_keep__ -delete if ! find ./wp-org-assets -mindepth 1 -print -quit | grep -q .; then diff --git a/.github/workflows/reusable-scripts-styles-lint.yml b/.github/workflows/reusable-scripts-styles-lint.yml index 6dd80eb..3fafbbf 100644 --- a/.github/workflows/reusable-scripts-styles-lint.yml +++ b/.github/workflows/reusable-scripts-styles-lint.yml @@ -33,7 +33,7 @@ jobs: fi lint: - name: ${{ matrix.script }} + name: Run ${{ matrix.script }} needs: validate-inputs runs-on: ubuntu-latest timeout-minutes: 30 @@ -57,7 +57,7 @@ jobs: exit 1 fi - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: ${{ inputs.project-path }}/package.json @@ -68,7 +68,7 @@ jobs: working-directory: ${{ inputs.project-path }} run: npm ci - - name: npm run ${{ matrix.script }} + - name: Run npm run ${{ matrix.script }} working-directory: ${{ inputs.project-path }} env: SCRIPT: ${{ matrix.script }} diff --git a/.github/workflows/reusable-supply-chain-audit.yml b/.github/workflows/reusable-supply-chain-audit.yml index a689113..43409f3 100644 --- a/.github/workflows/reusable-supply-chain-audit.yml +++ b/.github/workflows/reusable-supply-chain-audit.yml @@ -44,7 +44,7 @@ jobs: permissions: {} steps: # Both audits off leaves no job to run, so the workflow succeeds having audited nothing. - - name: Require at least one audit enabled + - name: Require at least one enabled audit if: ${{ !inputs.composer-audit && !inputs.npm-audit }} run: | echo "::error::Enable composer-audit and/or npm-audit — with both false this audit reports success without auditing anything." @@ -53,7 +53,7 @@ jobs: # Both audits read the committed lockfiles, because installing first runs third-party code (Composer plugins, npm lifecycle scripts) before the audit. composer-audit: if: ${{ inputs.composer-audit }} - name: composer audit + name: Audit Composer dependencies runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -62,12 +62,13 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + - name: Set up PHP + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.5' coverage: none tools: composer:v2 - - name: composer audit + - name: Run composer audit working-directory: ${{ inputs.project-path }} env: AUDIT_FLAGS: ${{ inputs.composer-audit-flags }} @@ -123,7 +124,7 @@ jobs: npm-audit: if: ${{ inputs.npm-audit }} - name: npm audit + name: Audit npm dependencies runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -132,10 +133,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '26' - - name: npm audit + - name: Run npm audit working-directory: ${{ inputs.project-path }} env: AUDIT_FLAGS: ${{ inputs.npm-audit-flags }} diff --git a/.github/workflows/reusable-workflow-checks.yml b/.github/workflows/reusable-workflow-checks.yml index 000ca4a..3cde116 100644 --- a/.github/workflows/reusable-workflow-checks.yml +++ b/.github/workflows/reusable-workflow-checks.yml @@ -13,14 +13,13 @@ permissions: {} jobs: actionlint: - name: Actionlint + name: Run actionlint runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -92,7 +91,7 @@ jobs: done zizmor: - name: Zizmor + name: Run zizmor needs: detect-ghas runs-on: ubuntu-latest timeout-minutes: 30 @@ -105,8 +104,7 @@ jobs: # repo's findings never differ by which branch it takes. ZIZMOR_VERSION: '1.31.1' steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a4f2c1..6aff38d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ permissions: {} jobs: phpunit: - name: PHPUnit (PHP ${{ matrix.php-version }}) + name: Run PHPUnit (PHP ${{ matrix.php-version }}) strategy: fail-fast: false matrix: From 3761b95f6865fbd3436d04cd654b1b7a4dc5f6a8 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:22:32 +0100 Subject: [PATCH 05/15] Describe every workflow input in one voice The input descriptions, which docs/workflows.md calls the authoritative wording, used two voices: "e.g." and "Leave empty to" in the PHPUnit and Playwright workflows beside "such as" and "Empty uses" elsewhere. Seven had no final period, the syntax check's example showed literal backslashes because a single-quoted YAML string keeps them, the wp-env config input narrated a migration off the implicit dev and tests split, the WordPress version inputs named a consumer plugin's .wp-env.json although the config file is an input, and the Playwright script's description told callers to override the default with the default. Each description now takes the docs tables' voice, ends with a period and carries no backticks, dashes or abbreviations, and the Playwright table's wp-env-core row matches the PHPUnit one. Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/reusable-block-json-check.yml | 2 +- .github/workflows/reusable-codeql.yml | 2 +- .github/workflows/reusable-php-lint.yml | 4 ++-- .github/workflows/reusable-php-syntax-check.yml | 4 ++-- .github/workflows/reusable-phpunit.yml | 12 ++++++------ .github/workflows/reusable-playwright-e2e.yml | 14 +++++++------- .github/workflows/reusable-release.yml | 6 +++--- .github/workflows/reusable-scripts-styles-lint.yml | 2 +- .github/workflows/reusable-supply-chain-audit.yml | 12 ++++++------ docs/workflows.md | 4 ++-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/reusable-block-json-check.yml b/.github/workflows/reusable-block-json-check.yml index d0ff7d5..ba2ae72 100644 --- a/.github/workflows/reusable-block-json-check.yml +++ b/.github/workflows/reusable-block-json-check.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: project-path: - description: 'Path to the project (relative to repo root)' + description: 'Path to the project, relative to the repository root.' type: string required: false default: '.' diff --git a/.github/workflows/reusable-codeql.yml b/.github/workflows/reusable-codeql.yml index 31c50da..383af46 100644 --- a/.github/workflows/reusable-codeql.yml +++ b/.github/workflows/reusable-codeql.yml @@ -5,7 +5,7 @@ on: inputs: languages: # CodeQL ships no PHP analyzer; PHP is statically analysed by PHPStan in the Quality workflow. - description: 'Non-empty JSON array of CodeQL languages' + description: 'Non-empty JSON array of CodeQL languages.' type: string required: false default: '["actions"]' diff --git a/.github/workflows/reusable-php-lint.yml b/.github/workflows/reusable-php-lint.yml index 3016617..1e155d3 100644 --- a/.github/workflows/reusable-php-lint.yml +++ b/.github/workflows/reusable-php-lint.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: scripts: - description: 'Non-empty JSON array of composer scripts; each runs as its own parallel job.' + description: 'Non-empty JSON array of Composer scripts; each runs as its own parallel job.' type: string required: true php-version: @@ -13,7 +13,7 @@ on: required: false default: '8.5' project-path: - description: 'Path to the project (relative to repo root).' + description: 'Path to the project, relative to the repository root.' type: string required: false default: '.' diff --git a/.github/workflows/reusable-php-syntax-check.yml b/.github/workflows/reusable-php-syntax-check.yml index d7ea085..1bafeba 100644 --- a/.github/workflows/reusable-php-syntax-check.yml +++ b/.github/workflows/reusable-php-syntax-check.yml @@ -4,12 +4,12 @@ on: workflow_call: inputs: project-path: - description: 'Path to the project (relative to repo root)' + description: 'Path to the project, relative to the repository root.' type: string required: false default: '.' php-versions: - description: 'Non-empty JSON array of PHP versions to check (e.g. [\"8.5\",\"8.6\"])' + description: 'Non-empty JSON array of PHP versions to check, such as ["8.5", "8.6"].' type: string required: false default: '["8.5", "8.6"]' diff --git a/.github/workflows/reusable-phpunit.yml b/.github/workflows/reusable-phpunit.yml index d186775..d7eefd2 100644 --- a/.github/workflows/reusable-phpunit.yml +++ b/.github/workflows/reusable-phpunit.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: project-path: - description: 'Path to the project (relative to repo root)' + description: 'Path to the project, relative to the repository root.' type: string required: false default: '.' @@ -14,17 +14,17 @@ on: required: false default: '8.5' wp-version: - description: 'WordPress version tag (e.g., "6.9.4"). Leave empty to defer to the consumer plugin''s .wp-env.json `core` setting (or wp-env''s default latest stable).' + description: 'WordPress release tag, such as "6.9.4". Empty defers to the wp-env config''s core, or wp-env''s latest stable release.' type: string required: false default: '' wp-env-core: - description: 'Full WP_ENV_CORE value. Overrides wp-version when set. Accepts repo refs (Org/Repo#ref) or zip URLs.' + description: 'Full WP_ENV_CORE value, such as a repository ref (Org/Repo#ref) or a zip URL. Overrides wp-version.' type: string required: false default: '' composer-script: - description: 'Composer script to invoke for the test run (e.g., test, test:integration).' + description: 'Composer script that runs the suite, such as test or test:integration.' type: string required: false default: 'test' @@ -34,12 +34,12 @@ on: required: false default: '--prefer-dist --ignore-platform-req=php+' wp-env-config-file: - description: 'Path to a wp-env config file (relative to project-path). Leave empty to use the default .wp-env.json. Use a separate file per environment (e.g., .wp-env.tests.json) to migrate off the deprecated implicit dev/tests split.' + description: 'wp-env config file relative to project-path, one per environment, such as .wp-env.tests.json. Empty uses .wp-env.json.' type: string required: false default: '' wp-env-xdebug: - description: 'Value for wp-env start --xdebug=; e.g. ''coverage'' for in-container coverage drivers. Leave empty to start without Xdebug.' + description: 'Mode for wp-env start --xdebug=, such as ''coverage'' for in-container coverage drivers. Empty starts without Xdebug.' type: string required: false default: '' diff --git a/.github/workflows/reusable-playwright-e2e.yml b/.github/workflows/reusable-playwright-e2e.yml index d1d6f0a..2d3de9f 100644 --- a/.github/workflows/reusable-playwright-e2e.yml +++ b/.github/workflows/reusable-playwright-e2e.yml @@ -4,12 +4,12 @@ on: workflow_call: inputs: project-path: - description: 'Path to the project (relative to repo root). npm ci runs here, on the Node version package.json declares, so package.json and package-lock.json must be present and declare @wordpress/env, whose locked version the workflow runs.' + description: 'Path to the project, relative to the repository root. npm ci runs here, on the Node version package.json declares, so package.json and package-lock.json must be present and declare @wordpress/env, whose locked version the workflow runs.' type: string required: false default: '.' artifact-slug: - description: 'Label for the uploaded failure-report artifact' + description: 'Label for the uploaded failure-report artifact.' type: string required: true php-version: @@ -18,12 +18,12 @@ on: required: false default: '8.5' wp-version: - description: 'WordPress version tag (e.g., "6.9.4"). Leave empty to defer to the consumer plugin''s .wp-env.json `core` setting (or wp-env''s default latest stable).' + description: 'WordPress release tag, such as "6.9.4". Empty defers to the wp-env config''s core, or wp-env''s latest stable release.' type: string required: false default: '' wp-env-core: - description: 'Full WP_ENV_CORE value. Overrides wp-version when set. Accepts repo refs (Org/Repo#ref) or zip URLs.' + description: 'Full WP_ENV_CORE value, such as a repository ref (Org/Repo#ref) or a zip URL. Overrides wp-version.' type: string required: false default: '' @@ -33,17 +33,17 @@ on: required: false default: '--prefer-dist --ignore-platform-req=php+' build-script: - description: 'npm script to build assets before starting wp-env. Leave empty to skip (plugins without a frontend build).' + description: 'npm script that builds assets before wp-env starts. Empty skips the build, for a plugin without a frontend build.' type: string required: false default: 'build' wp-env-config-file: - description: 'Path to a wp-env config file (relative to project-path). Leave empty to use the default .wp-env.json.' + description: 'wp-env config file relative to project-path. Empty uses .wp-env.json.' type: string required: false default: '' playwright-script: - description: 'npm script that runs the Playwright suite. Override when the plugin wires test:e2e or similar.' + description: 'npm script that runs the Playwright suite.' type: string required: false default: 'test:e2e' diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index 955079e..504614f 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -9,7 +9,7 @@ on: type: string required: true project-path: - description: 'Path to the project (relative to repo root). npm ci runs here, so package.json and package-lock.json are required, declaring a Node version and @wordpress/env, whose locked version runs the built-artifact test.' + description: 'Path to the project, relative to the repository root. npm ci runs here, so package.json and package-lock.json are required, declaring a Node version and @wordpress/env, whose locked version runs the built-artifact test.' type: string required: false default: '.' @@ -24,7 +24,7 @@ on: required: false default: '' plugin-check: - description: 'Whether to run WordPress Plugin Check against the built plugin archive' + description: 'Whether to run WordPress Plugin Check against the built plugin archive.' type: boolean required: false default: true @@ -34,7 +34,7 @@ on: required: false default: '' generate-pot: - description: 'Regenerate languages/.pot via wp i18n make-pot before archive creation; set false only for plugins with no translatable strings' + description: 'Whether to regenerate languages/.pot with wp i18n make-pot before the archive is built. Set false only for a plugin with no translatable strings.' type: boolean required: false default: true diff --git a/.github/workflows/reusable-scripts-styles-lint.yml b/.github/workflows/reusable-scripts-styles-lint.yml index 3fafbbf..a1f2fec 100644 --- a/.github/workflows/reusable-scripts-styles-lint.yml +++ b/.github/workflows/reusable-scripts-styles-lint.yml @@ -8,7 +8,7 @@ on: type: string required: true project-path: - description: 'Path to the project (relative to repo root). npm ci runs here, so package.json and a package-lock.json must be present, and package.json must declare the Node version.' + description: 'Path to the project, relative to the repository root. npm ci runs here, so package.json and a package-lock.json must be present, and package.json must declare the Node version.' type: string required: false default: '.' diff --git a/.github/workflows/reusable-supply-chain-audit.yml b/.github/workflows/reusable-supply-chain-audit.yml index 43409f3..ffe47ac 100644 --- a/.github/workflows/reusable-supply-chain-audit.yml +++ b/.github/workflows/reusable-supply-chain-audit.yml @@ -4,32 +4,32 @@ on: workflow_call: inputs: project-path: - description: 'Path to the project (relative to repo root). Its composer.lock and package-lock.json are audited as committed; nothing is installed.' + description: 'Path to the project, relative to the repository root. Its composer.lock and package-lock.json are audited as committed; nothing is installed.' type: string required: false default: '.' composer-audit: - description: 'Run `composer audit` against composer.lock, so the lockfile is required. Set false for repos without composer.json. At least one of composer-audit / npm-audit must stay enabled.' + description: 'Whether to run composer audit against composer.lock, so the lockfile is required. Set false for a project without composer.json. At least one of composer-audit and npm-audit stays enabled.' type: boolean required: false default: true npm-audit: - description: 'Run `npm audit` against package-lock.json, so the lockfile is required. Set false for repos without package.json. At least one of composer-audit / npm-audit must stay enabled.' + description: 'Whether to run npm audit against package-lock.json, so the lockfile is required. Set false for a project without package.json. At least one of composer-audit and npm-audit stays enabled.' type: boolean required: false default: true composer-audit-flags: - description: 'Extra flags passed to `composer audit`. Defaults to --abandoned=report — audits the full graph (dev/build deps included) and fails on security advisories, but reports abandoned packages instead of failing on them. Pass --no-dev to audit production deps only.' + description: 'Extra composer audit flags. The default, --abandoned=report, audits the full graph, development and build dependencies included, and fails on security advisories but only reports abandoned packages. Pass --no-dev to audit the production dependencies only.' type: string required: false default: '--abandoned=report' npm-audit-flags: - description: 'Extra flags passed to `npm audit`. The default audits production dependencies only and fails on high and critical advisories, surfacing low and moderate ones without failing; drop --omit=dev to audit development dependencies too.' + description: 'Extra npm audit flags. The default audits production dependencies only and fails on high and critical advisories, surfacing low and moderate ones without failing; drop --omit=dev to audit development dependencies too.' type: string required: false default: '--omit=dev --audit-level=high' fail-on-findings: - description: 'Fail the job when an audit reports findings. Set false to run the audit advisory-only — findings are surfaced in the logs without blocking, except a Composer filter-list match such as the malware list, which always fails.' + description: 'Whether findings fail the job. Set false to run the audit advisory-only, surfacing findings in the logs without blocking, except a Composer filter-list match such as the malware list, which always fails.' type: boolean required: false default: true diff --git a/docs/workflows.md b/docs/workflows.md index 6d956ec..c24ef3d 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -150,8 +150,8 @@ Installs Composer and npm dependencies, builds assets, installs Chromium, starts | `project-path` | `string` | No | `'.'` | Path to the project, relative to the repository root. | | `artifact-slug` | `string` | Yes | — | Label for the uploaded failure-report artifact. | | `php-version` | `string` | No | `'8.5'` | PHP for Composer, for any suite outside wp-env, and for the site when the wp-env config pins no `phpVersion`. | -| `wp-version` | `string` | No | `''` | WordPress release tag. Empty defers to the wp-env config's `core`. | -| `wp-env-core` | `string` | No | `''` | Full `WP_ENV_CORE` value. Overrides `wp-version`. | +| `wp-version` | `string` | No | `''` | WordPress release tag, such as `'6.9.4'`. Empty defers to the wp-env config's `core`, or wp-env's latest stable release. | +| `wp-env-core` | `string` | No | `''` | Full `WP_ENV_CORE` value, such as a repository ref or a zip URL. Overrides `wp-version`. | | `composer-options` | `string` | No | `'--prefer-dist --ignore-platform-req=php+'` | Options for the dependency install, which includes development packages so a scoping pipeline runs. | | `build-script` | `string` | No | `'build'` | npm script that builds assets before wp-env starts. Empty skips the build. | | `wp-env-config-file` | `string` | No | `''` | wp-env config file relative to `project-path`. Empty uses `.wp-env.json`. | From 1ab7a8331df6274cd804b1d29d9aa979264a0310 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:27:01 +0100 Subject: [PATCH 06/15] Write each CI comment as one capitalized sentence Several workflow comments broke the repository's one-sentence rule: two sentences or two clauses joined by a semicolon or a dash, a sentence wrapped over two lines, a lowercase tool name opening the line, and abbreviations such as "repo". The workflow checks restated the integer check's reason in a second comment, the block.json check described the joined string it avoids, and the CodeQL input named a caller's Quality workflow. The wp.org assets placeholder carried no reason, though the deploy job's download depends on it. Each comment now reads as one capitalized sentence on one line, the duplicate goes, and the placeholder says why it exists. Assisted-by: Claude Code:claude-opus-5-5 --- .github/actionlint.yml | 2 +- .github/workflows/quality.yml | 8 +++----- .../workflows/reusable-block-json-check.yml | 5 ++--- .github/workflows/reusable-codeql.yml | 4 ++-- .github/workflows/reusable-php-lint.yml | 2 +- .../workflows/reusable-php-syntax-check.yml | 2 +- .github/workflows/reusable-phpunit.yml | 2 +- .github/workflows/reusable-playwright-e2e.yml | 2 +- .github/workflows/reusable-release.yml | 19 ++++++++++--------- .../reusable-scripts-styles-lint.yml | 2 +- .../workflows/reusable-supply-chain-audit.yml | 2 +- .../workflows/reusable-workflow-checks.yml | 7 ++----- 12 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index aff04c3..c25328b 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -1,4 +1,4 @@ -# actionlint does not know the job context's workflow_repository and workflow_sha yet, which GitHub documents for a reusable workflow's own commit. +# The job context's workflow_repository and workflow_sha, which GitHub documents for a reusable workflow's own commit, are not yet known to actionlint. paths: .github/workflows/reusable-block-json-check.yml: ignore: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9ff56e1..043b702 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -42,8 +42,7 @@ jobs: phpcs-smoke: name: Smoke-test the PHPCS production profile - # Self-lint runs the shared profiles only through the root rulesets, which strip the WP-runtime - # sniffs consumers get — this job scans a plugin-shaped fixture with the raw production profile. + # Self-lint reaches the shared profiles only through the root rulesets, which drop the WordPress runtime sniffs consumers get, so this job scans a plugin-shaped fixture with the production profile itself. runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -65,8 +64,7 @@ jobs: with: composer-options: '--prefer-dist --ignore-platform-req=php+' - # phpcs -e loads the ruleset and lists its sniffs without scanning any file — the cheapest - # possible proof that the XML parses and every still resolves. + # Explaining the profile loads it and lists its sniffs without scanning a file, the cheapest proof that the XML parses and every rule reference still resolves. - name: Explain php/quality-assurance/phpcs.dist.xml run: vendor/bin/phpcs -e --standard=php/quality-assurance/phpcs.dist.xml @@ -113,7 +111,7 @@ jobs: block-json: name: Check block.json files - # Dogfoods reusable-block-json-check against a throwaway fixture; this tooling repo ships no real blocks. + # This repository ships no blocks, so the check runs against a fixture. permissions: contents: read uses: ./.github/workflows/reusable-block-json-check.yml diff --git a/.github/workflows/reusable-block-json-check.yml b/.github/workflows/reusable-block-json-check.yml index ba2ae72..e26a265 100644 --- a/.github/workflows/reusable-block-json-check.yml +++ b/.github/workflows/reusable-block-json-check.yml @@ -47,14 +47,13 @@ jobs: env: AJV: ${{ github.workspace }}/.block-json-tools/node/block-json/node_modules/.bin/ajv run: | - # Validate one file per -d invocation; a single joined string breaks multi-block plugins. + # Each file takes its own -d argument, so a plugin with several blocks validates every one. mapfile -d '' files < <(find . -path ./node_modules -prune -o -path ./vendor -prune -o -path ./.block-json-tools -prune -o -name 'block.json' -print0) if [ "${#files[@]}" -eq 0 ]; then echo "No block.json files found — nothing to validate." exit 0 fi - # schemas.wp.org publishes no versioned block.json path; trunk is the only (mutable) schema source. - # ajv-cli reads -s from disk and picks its parser from the extension, so fetch to a .json path; curl -f fails this check on a schemas.wp.org outage. + # The trunk schema, the only block.json schema schemas.wp.org publishes, is fetched to a .json path, because ajv-cli reads -s from disk and picks its parser from the extension, and curl -f fails the check during an outage. schema="$(mktemp -d)/schema.json" curl -fsSL https://schemas.wp.org/trunk/block.json -o "$schema" for file in "${files[@]}"; do diff --git a/.github/workflows/reusable-codeql.yml b/.github/workflows/reusable-codeql.yml index 383af46..9dbc1ee 100644 --- a/.github/workflows/reusable-codeql.yml +++ b/.github/workflows/reusable-codeql.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: languages: - # CodeQL ships no PHP analyzer; PHP is statically analysed by PHPStan in the Quality workflow. + # CodeQL has no PHP analyzer, so PHPStan covers PHP. description: 'Non-empty JSON array of CodeQL languages.' type: string required: false @@ -19,7 +19,7 @@ jobs: timeout-minutes: 30 permissions: {} steps: - # An empty JSON array expands to zero matrix jobs and reports success — a silent skip of the security scan. + # An empty JSON array expands to zero matrix jobs that report success, silently skipping the security scan. - name: Reject an empty languages array env: LANGUAGES: ${{ inputs.languages }} diff --git a/.github/workflows/reusable-php-lint.yml b/.github/workflows/reusable-php-lint.yml index 1e155d3..a157730 100644 --- a/.github/workflows/reusable-php-lint.yml +++ b/.github/workflows/reusable-php-lint.yml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 30 permissions: {} steps: - # An empty JSON array expands to zero matrix jobs and reports success — a silent skip of the lint. + # An empty JSON array expands to zero matrix jobs that report success, silently skipping the lint. - name: Reject an empty scripts array env: SCRIPTS: ${{ inputs.scripts }} diff --git a/.github/workflows/reusable-php-syntax-check.yml b/.github/workflows/reusable-php-syntax-check.yml index 1bafeba..e56e94b 100644 --- a/.github/workflows/reusable-php-syntax-check.yml +++ b/.github/workflows/reusable-php-syntax-check.yml @@ -33,7 +33,7 @@ jobs: timeout-minutes: 30 permissions: {} steps: - # An empty JSON array expands to zero matrix jobs and reports success — a silent skip of the syntax check. + # An empty JSON array expands to zero matrix jobs that report success, silently skipping the syntax check. - name: Reject an empty php-versions array env: PHP_VERSIONS: ${{ inputs.php-versions }} diff --git a/.github/workflows/reusable-phpunit.yml b/.github/workflows/reusable-phpunit.yml index d7eefd2..4386969 100644 --- a/.github/workflows/reusable-phpunit.yml +++ b/.github/workflows/reusable-phpunit.yml @@ -112,7 +112,7 @@ jobs: exit 1 fi - # wp-env applies WP_ENV_PHP_VERSION over a config's phpVersion, so a pinned site keeps its PHP while Composer and the suite run on php-version. + # Since wp-env applies WP_ENV_PHP_VERSION over a config's phpVersion, a pinned site keeps its PHP while Composer and the suite run on php-version. - name: Resolve the site's PHP if: ${{ inputs.needs-wp-env }} working-directory: ${{ inputs.project-path }} diff --git a/.github/workflows/reusable-playwright-e2e.yml b/.github/workflows/reusable-playwright-e2e.yml index 2d3de9f..3a285ab 100644 --- a/.github/workflows/reusable-playwright-e2e.yml +++ b/.github/workflows/reusable-playwright-e2e.yml @@ -127,7 +127,7 @@ jobs: working-directory: ${{ inputs.project-path }} run: npx --no-install playwright install --with-deps chromium - # wp-env applies WP_ENV_PHP_VERSION over a config's phpVersion, so a pinned site keeps its PHP while Composer and the suite run on php-version. + # Since wp-env applies WP_ENV_PHP_VERSION over a config's phpVersion, a pinned site keeps its PHP while Composer and the suite run on php-version. - name: Resolve the site's PHP working-directory: ${{ inputs.project-path }} env: diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index 504614f..3b72da6 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -1,5 +1,5 @@ name: Release -# Callers grant `contents: write`, `actions: read`, `id-token: write` and `attestations: write`; a caller must grant every permission the called jobs declare, or the call fails at startup. +# A caller grants contents: write, actions: read, id-token: write and attestations: write, every permission the called jobs declare, or the call fails at startup. on: workflow_call: @@ -110,7 +110,7 @@ jobs: echo "tag-version=$tag_version" >> "$GITHUB_OUTPUT" - # A GitHub-distributed build carries an Update URI and no readme.txt by design; wp.org builds run the full set. + # A GitHub-distributed build carries an Update URI and no readme.txt by design, while a wp.org build runs the full set. - name: Resolve the excluded checks id: excluded if: ${{ inputs.plugin-check }} @@ -189,7 +189,7 @@ jobs: exit 0 fi notes=$(mktemp) - # index() matches the heading as text; a regex would read the version's dots as wildcards. + # The heading is matched as literal text with index(), so a dot in the version matches only a dot. awk -v v="$VERSION" ' $0 == "## " v || index($0, "## " v " ") == 1 || index($0, "## [" v "]") == 1 { capture = 1; next } capture && /^## / { exit } @@ -288,7 +288,7 @@ jobs: exit 1 fi - # --no-scripts avoids invoking handlers that were just pruned from require-dev. + # The prune runs no scripts, because their handlers leave with the development dependencies. - name: Prune to the production dependencies working-directory: ${{ inputs.project-path }} run: composer install --no-dev --no-interaction --no-progress --prefer-dist --ignore-platform-req=php+ --optimize-autoloader --no-scripts @@ -523,6 +523,7 @@ jobs: if [ -d .wordpress-org ]; then cp -R .wordpress-org/. wp-org-assets-artifact/ fi + # A placeholder keeps the artifact non-empty, so the deploy job's download succeeds for a plugin without .wordpress-org/. touch wp-org-assets-artifact/__artifact_keep__ - name: Upload the wp.org assets artifact @@ -543,7 +544,7 @@ jobs: env: PLUGIN_SLUG: ${{ inputs.plugin-slug }} WP_ENV_CONFIG_FILE: ${{ inputs.wp-env-config-file }} - # wp-env lets this variable win over the config's phpVersion, so the artifact runs on the PHP the build was checked on. + # Since wp-env lets this variable win over the config's phpVersion, the artifact runs on the PHP the build was checked on. WP_ENV_PHP_VERSION: ${{ inputs.php-version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -606,7 +607,7 @@ jobs: echo "::error::Release contract violation — $config must exist and map \"$key\" to the plugin source; found mappings: $(jq -c '.mappings // {}' "$config" 2> /dev/null || echo 'none')." exit 1 fi - # wp-env layers env-scoped settings over root, so the redirect and the latest core cover root plus development and tests, or an env-scoped mapping or core pin would run the E2E against source. + # Since wp-env layers env-scoped settings over the root ones, the redirect and the latest core cover the root, development and tests, or an env-scoped mapping or core pin would run the E2E tests against the source. override="${config%.json}.override.json" [ -f "$override" ] || echo '{}' > "$override" jq --arg key "$key" --arg built "$BUILT_PATH" '. * ({core: null, mappings: {($key): $built}} | . + {env: {development: ., tests: .}})' "$override" > "$override.tmp" @@ -647,7 +648,7 @@ jobs: exit 1 fi run_cli wp plugin activate "$PLUGIN_SLUG" - # wp-env's run consumes a `--`, so the argument after the script is a placeholder for bash's $0. + # Since wp-env's run consumes a --, the argument after the script is a placeholder for bash's $0. # shellcheck disable=SC2016 run_cli bash -c 'got=$(wp plugin get "$1" --field=version); echo "Active plugin version: $got (expected $2)"; test "$got" = "$2"' bash "$PLUGIN_SLUG" "$VERSION" @@ -673,7 +674,7 @@ jobs: provenance: name: Verify the release provenance - # A dispatched dry run rehearses this check; a pull request's merge commit has no push runs, and a push-triggered dry run would race the runs it looks up. + # Only a dispatched dry run rehearses this check, because a pull request's merge commit has no push runs and a push-triggered dry run would race the runs it looks up. if: ${{ inputs.publish || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 30 @@ -773,7 +774,7 @@ jobs: wp-org: name: Deploy to wp.org needs: [build, test, provenance, release] - # wp.org offers sites only its Stable tag, so a prerelease ships through GitHub alone. + # Sites receive only a wp.org plugin's Stable tag, so a prerelease ships through GitHub alone. if: ${{ inputs.publish && inputs.wp-org && !contains(needs.build.outputs.version, '-') }} runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/reusable-scripts-styles-lint.yml b/.github/workflows/reusable-scripts-styles-lint.yml index a1f2fec..8d5c6d1 100644 --- a/.github/workflows/reusable-scripts-styles-lint.yml +++ b/.github/workflows/reusable-scripts-styles-lint.yml @@ -22,7 +22,7 @@ jobs: timeout-minutes: 30 permissions: {} steps: - # An empty JSON array expands to zero matrix jobs and reports success — a silent skip of the lint. + # An empty JSON array expands to zero matrix jobs that report success, silently skipping the lint. - name: Reject an empty scripts array env: SCRIPTS: ${{ inputs.scripts }} diff --git a/.github/workflows/reusable-supply-chain-audit.yml b/.github/workflows/reusable-supply-chain-audit.yml index ffe47ac..a7db8a6 100644 --- a/.github/workflows/reusable-supply-chain-audit.yml +++ b/.github/workflows/reusable-supply-chain-audit.yml @@ -76,7 +76,7 @@ jobs: run: | audit_json="$(mktemp)" status=0 - # Composer has no distinct findings exit code, so the JSON report decides; advisory mode softens advisories and abandoned packages, never a filter-list match. + # Composer has no distinct findings exit code, so the JSON report decides, and advisory mode softens advisories and abandoned packages but never a filter-list match. # shellcheck disable=SC2086 composer audit --locked $AUDIT_FLAGS --format=json > "$audit_json" || status=$? cat "$audit_json" diff --git a/.github/workflows/reusable-workflow-checks.yml b/.github/workflows/reusable-workflow-checks.yml index 3cde116..3111c96 100644 --- a/.github/workflows/reusable-workflow-checks.yml +++ b/.github/workflows/reusable-workflow-checks.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - # actionlint reads only the root's .github/workflows unless it is given files, and then reads only those. + # Given files, actionlint reads only those, and otherwise only the root's .github/workflows. - name: Resolve the actionlint files id: files env: @@ -100,8 +100,7 @@ jobs: contents: read actions: read env: - # Both branches below resolve this release through the action's own version table, so a - # repo's findings never differ by which branch it takes. + # Both branches below resolve this release through the action's own version table, so a repository's findings never differ by the branch it takes. ZIZMOR_VERSION: '1.31.1' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -139,8 +138,6 @@ jobs: echo "::error::zizmor SARIF at '$SARIF' is missing or malformed — cannot confirm a clean scan." exit 1 fi - # Whole-string integer check (grep is line-oriented, so a multi-document count would slip - # through it); anything but a single non-negative integer is an unknown result. case "$count" in '' | *[!0-9]*) echo "::error::unexpected zizmor finding count '$count' — cannot confirm a clean scan." From 89b89453a05c51696010432a929440f7f376b8a3 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:28:01 +0100 Subject: [PATCH 07/15] Cut the baseline and fixture comments to what they explain The ESLint and Stylelint baselines each carried a second header line that restated the ignore list beneath it. The .distignore baseline's second line told the reader to copy the file, an instruction that stays behind as a wrong one in every copy, and its secret-material note ran over two lines with an aside that means nothing inside a copied file. The PHPCS smoke's plugin stub declared WordPress 7.0 under the profile's 7.1 floor, and its descriptions ran to a paragraph. The restating lines and the copy instruction go, the secret note is one sentence, and the stub declares 7.1 with one-line descriptions. Assisted-by: Claude Code:claude-opus-5-5 --- .distignore | 4 +--- node/eslint.config.base.mjs | 1 - node/stylelint.config.base.js | 1 - tests/Fixtures/plugin-stub/plugin-stub.php | 4 ++-- tests/Fixtures/plugin-stub/src/Example.php | 5 ++--- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.distignore b/.distignore index 1a6dcc5..1f7d225 100644 --- a/.distignore +++ b/.distignore @@ -1,5 +1,4 @@ # Baseline wp dist-archive exclusions for WordPress projects. -# Copy this file to each consumer project root and extend it there. .git .github @@ -38,8 +37,7 @@ scoper.inc.php .wp-env* .wordpress-org -# Secret material — never ship, even when a consumer forgets to list it. -# .pem is root-anchored (leading /) so certs bundled under vendor/ (e.g. CA bundles) still ship. +# Secret material, with .pem anchored to the root so certificates bundled under vendor/ still ship. .env* /*.pem *.key diff --git a/node/eslint.config.base.mjs b/node/eslint.config.base.mjs index bcfecb8..09b3ba9 100644 --- a/node/eslint.config.base.mjs +++ b/node/eslint.config.base.mjs @@ -1,6 +1,5 @@ /** * Shared flat ESLint baseline for WordPress projects; append project overrides after this array. - * Ignores cover dependencies and build output. */ import wordpress from '@wordpress/eslint-plugin'; diff --git a/node/stylelint.config.base.js b/node/stylelint.config.base.js index fdba812..205d5b0 100644 --- a/node/stylelint.config.base.js +++ b/node/stylelint.config.base.js @@ -1,6 +1,5 @@ /** * Spread this base into the consumer root config; `extends` drops `ignoreFiles`. - * ignoreFiles covers dependencies and build output. */ module.exports = { diff --git a/tests/Fixtures/plugin-stub/plugin-stub.php b/tests/Fixtures/plugin-stub/plugin-stub.php index 9306153..d78cd3d 100644 --- a/tests/Fixtures/plugin-stub/plugin-stub.php +++ b/tests/Fixtures/plugin-stub/plugin-stub.php @@ -1,12 +1,12 @@ Date: Sat, 7 Nov 2026 10:30:41 +0100 Subject: [PATCH 08/15] Declare the JSON failures and the case-sensitive check's reason The scoping pipeline decodes manifests and symbol lists with JSON_THROW_ON_ERROR, but no docblock named the \JsonException that escapes, which does not extend RuntimeException. The collector matches function_exists() case-sensitively beside the lowercased define() and class_alias() matches without saying why. GlobalTagVisitor's docblock narrated the code below its reason, its @var template braced a simple property, and the php-font-lib recipe comment joined two sentences with a semicolon. Each function a \JsonException leaves names it in @throws, the case-sensitive match states its reason, and the docblock and the comments are one sentence of why. Assisted-by: Claude Code:claude-opus-5-5 --- php/composer/GlobalSymbols.php | 2 ++ php/composer/Manifest.php | 2 ++ php/composer/ScopePhpDependencies.php | 3 +++ php/composer/ScopedPackages.php | 1 + php/composer/Visitors/ScopedSymbolCollector.php | 2 +- php/php-scoper/recipes/dompdf/php-font-lib.inc.php | 2 +- php/php-scoper/scoper-base.inc.php | 1 + php/phpstan/GlobalTagVisitor.php | 4 ++-- 8 files changed, 13 insertions(+), 4 deletions(-) diff --git a/php/composer/GlobalSymbols.php b/php/composer/GlobalSymbols.php index 57ac3f5..49f63b2 100644 --- a/php/composer/GlobalSymbols.php +++ b/php/composer/GlobalSymbols.php @@ -35,6 +35,7 @@ protected function __construct() {} * @param list $dropped_dirs The directories of the packages that stay unscoped. * * @throws \RuntimeException Thrown when a declared stubs file is missing, or a file cannot be read or does not parse. + * @throws \JsonException Thrown when a manifest or a symbol list is not valid JSON. * * @return array{classes: list, functions: list, constants: list, namespaces: list} */ @@ -112,6 +113,7 @@ public static function collect( string $project_dir, array $package_dirs, array * @param string $file The path of the file. * * @throws \RuntimeException Thrown when the file cannot be read or holds no list. + * @throws \JsonException Thrown when the file is not valid JSON. * * @return list */ diff --git a/php/composer/Manifest.php b/php/composer/Manifest.php index a835f83..08b212f 100644 --- a/php/composer/Manifest.php +++ b/php/composer/Manifest.php @@ -25,6 +25,7 @@ protected function __construct() {} * @param string $file The path of the file. * * @throws \RuntimeException Thrown when the file cannot be read. + * @throws \JsonException Thrown when the file is not valid JSON. * * @return array */ @@ -55,6 +56,7 @@ public static function extra( array $manifest ): array { * @param string $project_dir The root directory of the project. * * @throws \RuntimeException Thrown when the manifest cannot be read or declares no scoping prefix. + * @throws \JsonException Thrown when the manifest is not valid JSON. * * @return string */ diff --git a/php/composer/ScopePhpDependencies.php b/php/composer/ScopePhpDependencies.php index 6bf5844..a17ff06 100644 --- a/php/composer/ScopePhpDependencies.php +++ b/php/composer/ScopePhpDependencies.php @@ -38,6 +38,7 @@ protected function __construct() {} * @param Event $event The Composer script event. * * @throws \RuntimeException Thrown when the scope run fails. + * @throws \JsonException Thrown when a manifest is not valid JSON. * * @return void */ @@ -55,6 +56,7 @@ public static function postAutoloadDump( Event $event ): void { // phpcs:ignore * @param string $project_dir The root directory of the project. * * @throws \RuntimeException Thrown when the project, its scoping prefix or php-scoper is missing, vendor-prefixed is a symbolic link, php-scoper fails, a scoped file does not parse, a scoped package declares a class outside the prefix, a prefixed name is declared by no scoped package, or a scoped package manifest cannot be read. + * @throws \JsonException Thrown when a manifest is not valid JSON. * * @return void */ @@ -278,6 +280,7 @@ public static function scope( string $project_dir ): void { * @param string $file The path of the manifest. * * @throws \RuntimeException Thrown when the manifest cannot be read or declares no name. + * @throws \JsonException Thrown when the manifest is not valid JSON. * * @return string */ diff --git a/php/composer/ScopedPackages.php b/php/composer/ScopedPackages.php index 1eafb91..f9caad5 100644 --- a/php/composer/ScopedPackages.php +++ b/php/composer/ScopedPackages.php @@ -28,6 +28,7 @@ protected function __construct() {} * @param list $names The packages to scope, as names or fnmatch() patterns, where a name that starts with ! drops the packages it matches. * * @throws \RuntimeException Thrown when no name is given, installed.json or installed.php is unreadable, a name matches no installed package, a dependency is not installed, or a scoped package is a symbolic link. + * @throws \JsonException Thrown when installed.json is not valid JSON. * * @return array{scoped: array, dropped: array} */ diff --git a/php/composer/Visitors/ScopedSymbolCollector.php b/php/composer/Visitors/ScopedSymbolCollector.php index 38d22cd..55d8b2d 100644 --- a/php/composer/Visitors/ScopedSymbolCollector.php +++ b/php/composer/Visitors/ScopedSymbolCollector.php @@ -166,7 +166,7 @@ protected function reference( string $name, Node $node, array $kinds ): void { 'name' => $name, 'location' => $this->file . ':' . $node->getStartLine(), 'keys' => \array_combine( $kinds, \array_map( static fn ( string $kind ): string => self::key( $kind, $name ), $kinds ) ), - 'checked' => $call instanceof Node\Expr\FuncCall && $call->name instanceof Node\Name && 'function_exists' === $call->name->toString(), + 'checked' => $call instanceof Node\Expr\FuncCall && $call->name instanceof Node\Name && 'function_exists' === $call->name->toString(), // Only a lowercase function_exists() call gets the twin php-scoper adds. ); } } diff --git a/php/php-scoper/recipes/dompdf/php-font-lib.inc.php b/php/php-scoper/recipes/dompdf/php-font-lib.inc.php index 8198758..15e79c0 100644 --- a/php/php-scoper/recipes/dompdf/php-font-lib.inc.php +++ b/php/php-scoper/recipes/dompdf/php-font-lib.inc.php @@ -11,7 +11,7 @@ 'replacements' => array( // getFontType() reads the font type from the second segment of its class name, which the prefix shifts. 'src/FontLib/TrueType/File.php' => array( 'return $class_parts[1];' => 'return $class_parts[\count($class_parts) - 2];' ), - // The test names FontLib\glyf, a class no package declares, so it is always false; the global name keeps that meaning. + // The test names FontLib\glyf, a class no package declares, so it is always false, and the global name keeps that meaning. 'src/FontLib/AdobeFontMetrics.php' => array( '$glyf instanceof glyf' => '$glyf instanceof \FontLib\glyf' ), ), ); diff --git a/php/php-scoper/scoper-base.inc.php b/php/php-scoper/scoper-base.inc.php index de90421..5a3fd0b 100644 --- a/php/php-scoper/scoper-base.inc.php +++ b/php/php-scoper/scoper-base.inc.php @@ -17,6 +17,7 @@ * @param string ...$names The packages to scope, as names or fnmatch() patterns, where a name that starts with ! drops the packages it matches. * * @throws RuntimeException Thrown when composer.json declares no scoping prefix, or no text domain while a scoped package declares one, when a package to scope is missing, or when a recipe's target is missing. + * @throws JsonException Thrown when a manifest, installed.json or a symbol list is not valid JSON. * * @return array */ diff --git a/php/phpstan/GlobalTagVisitor.php b/php/phpstan/GlobalTagVisitor.php index 0214958..23edde3 100644 --- a/php/phpstan/GlobalTagVisitor.php +++ b/php/phpstan/GlobalTagVisitor.php @@ -12,7 +12,7 @@ /** * Gives each global statement the types that the @global tags of its function's or method's docblock declare, as WordPress's documentation standard writes them. * - * PHPStan types a global as mixed but reads an inline @var on the statement, so the visitor attaches one; a global no tag declares stays mixed, and a statement with its own docblock keeps it. + * PHPStan types a global as mixed but reads an inline @var on the statement. * * @internal */ @@ -78,7 +78,7 @@ public function enterNode( Node $node ): null { $lines = array(); foreach ( $node->vars as $var ) { if ( $var instanceof Node\Expr\Variable && \is_string( $var->name ) && isset( $types[ $var->name ] ) ) { - $lines[] = " * @var {$types[ $var->name ]} \${$var->name}"; + $lines[] = " * @var {$types[ $var->name ]} \$$var->name"; } } From 91098fbba1ffed50a9c40d81890549dff1d7bd7c Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:31:56 +0100 Subject: [PATCH 09/15] Move the scoping contract into docs/scoping.md The README is meant to be an index, but its Dependency scoping section carried the whole scoping contract inline: how names select packages, every step and failure of a run, the stubs and text-domain declarations, and the recipe format. Its Playwright paragraph ran to 120 words of factory internals, and its Development section repeated commands that CONTRIBUTING.md lists in full. docs/scoping.md now holds the contract, moved unchanged under a scope sentence and section headings, and the README keeps the installation steps and a link. The Playwright paragraph states where specs and artifacts live and how to replace the web server's command, the Development section links to CONTRIBUTING.md and the tests guide, and AGENTS.md names the new document. Assisted-by: Claude Code:claude-opus-5-5 --- AGENTS.md | 4 +-- README.md | 72 +++---------------------------------------------- docs/scoping.md | 61 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 70 deletions(-) create mode 100644 docs/scoping.md diff --git a/AGENTS.md b/AGENTS.md index 12e0875..dbb728e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ wordpress-configs/ │ └── release/ # the manifest and lock of the WP-CLI commands reusable-release.yml installs, which Dependabot keeps current ├── node/ # tsconfig, ESLint, Stylelint and Playwright baselines │ └── block-json/ # the manifest and lock of the ajv-cli reusable-block-json-check.yml installs, which the npm package leaves out -├── docs/ # workflows.md (input reference) and scripts-contract.md (scripts, files, floors) +├── docs/ # workflows.md (input reference), scripts-contract.md (scripts, files, floors) and scoping.md (the scoping contract) ├── tests/ │ ├── Unit/ # PHPUnit tests that run the real tools │ ├── Support/ # RunsWorkflowStepsTrait, which runs a workflow step's script, and RecordsInstalledPackagesTrait @@ -46,7 +46,7 @@ zizmor --config .github/zizmor.yml .github/ - Workflow-step tests find a step by its job and `name` and run it with `bash -e`, as GitHub Actions does for a step that names no shell; renaming a tested step breaks its test. - Every reusable-workflow input has a `description:`; shipped input names, types and defaults change only as a coordinated migration (CONTRIBUTING.md). - Self-CI calls the reusable workflows through `./`, and `.github/zizmor.yml` turns off zizmor's `self-repository` audit until actionlint accepts `$/`. -- The README is an index; workflow inputs live in `docs/workflows.md`, script and file contracts in `docs/scripts-contract.md`, test tiers in `tests/README.md`. A behavior change updates them in the same change. +- The README is an index; workflow inputs live in `docs/workflows.md`, script and file contracts in `docs/scripts-contract.md`, the scoping contract in `docs/scoping.md`, test tiers in `tests/README.md`. A behavior change updates them in the same change. - The README names each floor by the file and key that hold it (`composer.json`'s `require.php`, `package.json`'s `engines`, `phpcs.base.dist.xml`'s `testVersion` and `minimum_wp_version`, `phpstan.dist.neon`'s `requiresAtLeast`), never by its number, so a floor change edits only those files. - Class and method docblocks open with one third-person sentence naming the role, such as "Returns …", and add a second paragraph only for a non-obvious why; property and constant docblocks open with "The …". A function or method docblock ends with `@return`, `@return void` included, unless it documents a constructor or opens with `{@inheritDoc}`. Each `@param` stays on one line, `@throws` reads "Thrown when …", members are `protected` rather than `private`, and class bodies outside `tests/` group members in `// region NAME` blocks. - Inline comments state a non-obvious why in one capitalized sentence, and every file describes the present, never the change. diff --git a/README.md b/README.md index 3fd2d36..f6e98f4 100644 --- a/README.md +++ b/README.md @@ -170,56 +170,10 @@ Create `scoper.inc.php` at the project root and name the packages to scope: return ( require __DIR__ . '/vendor/ahegyes/wordpress-configs/php/php-scoper/scoper-base.inc.php' )( __DIR__, 'dompdf/dompdf', 'php-di/php-di' ); ``` -A name is a package name or an `fnmatch()` pattern, such as `'your-vendor/your-library-*'`. The runtime dependencies of each named package follow from Composer's `installed.json`, through `require` and never `require-dev`, so naming a library scopes what it needs; a requirement that another package or the project itself replaces or provides, as Composer records in `installed.php`, counts as met. A pattern matches every installed package, development tools included, so keep it as narrow as the packages it means. A name that starts with `!` keeps the packages it matches unscoped, such as a polyfill whose global functions must stay reachable (`'!symfony/polyfill-php80'`), and the symbols they declare stay global too. Keep the scoped packages in `require-dev`, so a production install never loads their unprefixed copies, and require an unscoped package in `require`, so a production install still ships it. - -A scope run: - -- scopes every file of each scoped package into `vendor-prefixed.tmp/` and, once php-scoper succeeds, moves it to `vendor-prefixed///` in place of the previous build, refusing a `vendor-prefixed/` that is a symbolic link, so a run php-scoper fails keeps the previous build; -- leaves global the WordPress symbols of `sniccowp/php-scoper-wordpress-excludes`, Action Scheduler (`as_*` and `ActionScheduler*`), every symbol of the stubs files the project or a scoped package declares under `extra.scoping-stubs`, and their namespaces; -- prefixes the strings that start with a scoped package's own namespace, which libraries use to build class names, unless they name a namespace the run leaves global or a symbol in one; -- applies the recipe of each scoped package that has one in `php/php-scoper/recipes//.inc.php`; -- rewrites each string literal of the text domain a scoped package declares under `extra.scoping-text-domain` to the project's `extra.text-domain`, unless the two are the same; -- fails, naming each one, when the scoped code references a prefixed name it does not declare as the kind of symbol the reference needs: a host symbol no stubs file declares, a dependency outside the scoped packages, a string php-scoper took for a class name, or the class of a `Class::MEMBER` string; -- fails, naming its namespace, when the scoped code declares a class, interface, trait or enum outside the prefix, which a copy the host or another plugin loads can replace, other than the polyfill of a PHP class; -- passes a `function_exists()` check of the prefixed name of a function it leaves global, PHP's own included, which php-scoper adds to every guard of that function and which is false while nothing declares the name; -- writes `vendor-prefixed/scoper-autoload.php`, a class map over each scoped package's `psr-4`, `psr-0` and `classmap` paths that honors `exclude-from-classmap`, followed by its `files` entries. - -The run also fails when `extra.scoping-prefix` is missing or empty, when a name matches no installed package, when a scoped package requires one that is not installed, when a declared stubs file is missing, when a scoped file, a declared stubs file or a dropped package's file does not parse, when a scoped package declares a text domain and the project declares none, when a declared text domain survives outside a string literal of its own, and when a recipe's target is gone. - -A package that reaches a dependency only through a metapackage includes its `files` entries before that dependency's, because the autoloader orders them by each package's direct `require`. - -The run supports none of php-scoper's `expose-*` options, since each plugin keeps its own prefixed copy of what it scopes: the namespace-string patcher prefixes the global name in the alias php-scoper writes for an exposed class, so the class stays unexposed. - -The project or a scoped package declares the stubs files of the host symbols the scoped code calls, each as `./path/to/file.php` relative to its `composer.json`, as `vendor/package` for that package's `.php`, or as `vendor/package:path/to/file.php`: - -```json -{ - "extra": { - "scoping-stubs": [ - "php-stubs/woocommerce-stubs", - "php-stubs/woocommerce-stubs:woocommerce-packages-stubs.php", - "./stubs/host-plugin.php" - ] - } -} -``` - -A package whose strings take the host plugin's text domain declares the domain it uses, and the project declares its own: - -```json -{ - "extra": { - "scoping-text-domain": "your-library" - } -} -``` - -`scoper.inc.php` returns an ordinary php-scoper config, so a project can add exclusions or patchers before returning it. Write namespace exclusions as anchored regular expressions, such as `'/^YourVendor\\\\Name(?:\\\\|$)/i'`, because php-scoper matches a plain namespace string anywhere in a name. - -A recipe returns php-scoper settings for one package, with paths relative to it: `exclude-files`, anchored `exclude-namespaces`, and `replacements`, a map of search to replace strings per file of the scoped code, in which `{prefix}` stands for the scoping prefix. A replacement whose search string is missing fails the run, so a library update that invalidates a recipe surfaces when scoping. - `postAutoloadDump` scopes only in development mode, so a production install keeps the scoped output it ships; `composer dump-autoload` scopes again by hand. +[The scoping contract](docs/scoping.md) gives the packages a run scopes, what it prefixes, leaves global and rejects, and how a project declares host stubs, text domains, its own php-scoper settings and per-package recipes. + ## Node baselines `node/` holds one baseline per tool, which a project extends and overrides. The package declares the tools behind them as `>=` peer dependencies, floored at the versions it is tested against: `@wordpress/scripts`, `@wordpress/eslint-plugin`, `@wordpress/stylelint-config`, `@playwright/test` and `typescript`, whose defaults `node/tsconfig.base.json` builds on. `typescript` stays below 7, because typescript-eslint, which the ESLint base loads, refuses TypeScript 7. @@ -265,7 +219,7 @@ const baseConfig = require( '@ahegyes/wordpress-configs/node/playwright.config.b module.exports = defineConfig( { ...baseConfig, use: { ...baseConfig.use } } ); ``` -The Playwright factory sets `testDir` to `tests/EndToEnd/` and artifacts under `tests/.cache/artifacts/`, and returns the `@wordpress/scripts` `webServer` on the given port, which runs `npm run wp-env start`. It sets `WP_BASE_URL` and `WP_ARTIFACTS_PATH` only when they are unset, before loading the `@wordpress/scripts` config, which reads them once, so an exported value wins over `port` and a second call in the same process reuses the first configuration; a project that starts wp-env through its own script spreads `webServer` and sets `command`. For WordPress fixtures, import from `@wordpress/e2e-test-utils-playwright`; for accessibility scans, add `@axe-core/playwright` to the project's own devDependencies. +The factory reads the specs from `tests/EndToEnd/` and writes artifacts under `tests/.cache/artifacts/`. Its `webServer` runs `npm run wp-env start` on the given port, so a project that starts wp-env through another script spreads `webServer` and sets `command`. ## Reusable workflows @@ -295,22 +249,4 @@ The Playwright factory sets `testDir` to `tests/EndToEnd/` and artifacts under ` ## Development -Install the dependencies: - -```sh -composer install -``` - -Run the unit suite: - -```sh -composer test -``` - -Validate the manifest and run the PHP lint and the unit suite together: - -```sh -composer quality-check -``` - -[The tests guide](tests/README.md) describes the test tiers, the opt-in mutation run and the fixtures, and [CONTRIBUTING.md](CONTRIBUTING.md) covers the checks to run before a pull request. +[CONTRIBUTING.md](CONTRIBUTING.md) gives the setup and the checks to run before a pull request, and [the tests guide](tests/README.md) describes the test tiers, the opt-in mutation run and the fixtures. diff --git a/docs/scoping.md b/docs/scoping.md new file mode 100644 index 0000000..49b2477 --- /dev/null +++ b/docs/scoping.md @@ -0,0 +1,61 @@ +# Dependency scoping + +This document is the contract of the scoping pipeline that `php/php-scoper/scoper-base.inc.php` and `php/composer/ScopePhpDependencies.php` run: the packages a project names, what a scope run prefixes, leaves global and rejects, and how a project declares host stubs, text domains, its own php-scoper settings and per-package recipes. [The README](../README.md#dependency-scoping) shows how a project installs it. + +## Packages to scope + +A name is a package name or an `fnmatch()` pattern, such as `'your-vendor/your-library-*'`. The runtime dependencies of each named package follow from Composer's `installed.json`, through `require` and never `require-dev`, so naming a library scopes what it needs; a requirement that another package or the project itself replaces or provides, as Composer records in `installed.php`, counts as met. A pattern matches every installed package, development tools included, so keep it as narrow as the packages it means. A name that starts with `!` keeps the packages it matches unscoped, such as a polyfill whose global functions must stay reachable (`'!symfony/polyfill-php80'`), and the symbols they declare stay global too. Keep the scoped packages in `require-dev`, so a production install never loads their unprefixed copies, and require an unscoped package in `require`, so a production install still ships it. + +## A scope run + +A scope run: + +- scopes every file of each scoped package into `vendor-prefixed.tmp/` and, once php-scoper succeeds, moves it to `vendor-prefixed///` in place of the previous build, refusing a `vendor-prefixed/` that is a symbolic link, so a run php-scoper fails keeps the previous build; +- leaves global the WordPress symbols of `sniccowp/php-scoper-wordpress-excludes`, Action Scheduler (`as_*` and `ActionScheduler*`), every symbol of the stubs files the project or a scoped package declares under `extra.scoping-stubs`, and their namespaces; +- prefixes the strings that start with a scoped package's own namespace, which libraries use to build class names, unless they name a namespace the run leaves global or a symbol in one; +- applies the recipe of each scoped package that has one in `php/php-scoper/recipes//.inc.php`; +- rewrites each string literal of the text domain a scoped package declares under `extra.scoping-text-domain` to the project's `extra.text-domain`, unless the two are the same; +- fails, naming each one, when the scoped code references a prefixed name it does not declare as the kind of symbol the reference needs: a host symbol no stubs file declares, a dependency outside the scoped packages, a string php-scoper took for a class name, or the class of a `Class::MEMBER` string; +- fails, naming its namespace, when the scoped code declares a class, interface, trait or enum outside the prefix, which a copy the host or another plugin loads can replace, other than the polyfill of a PHP class; +- passes a `function_exists()` check of the prefixed name of a function it leaves global, PHP's own included, which php-scoper adds to every guard of that function and which is false while nothing declares the name; +- writes `vendor-prefixed/scoper-autoload.php`, a class map over each scoped package's `psr-4`, `psr-0` and `classmap` paths that honors `exclude-from-classmap`, followed by its `files` entries. + +The run also fails when `extra.scoping-prefix` is missing or empty, when a name matches no installed package, when a scoped package requires one that is not installed, when a declared stubs file is missing, when a scoped file, a declared stubs file or a dropped package's file does not parse, when a scoped package declares a text domain and the project declares none, when a declared text domain survives outside a string literal of its own, and when a recipe's target is gone. + +A package that reaches a dependency only through a metapackage includes its `files` entries before that dependency's, because the autoloader orders them by each package's direct `require`. + +The run supports none of php-scoper's `expose-*` options, since each plugin keeps its own prefixed copy of what it scopes: the namespace-string patcher prefixes the global name in the alias php-scoper writes for an exposed class, so the class stays unexposed. + +## Host stubs + +The project or a scoped package declares the stubs files of the host symbols the scoped code calls, each as `./path/to/file.php` relative to its `composer.json`, as `vendor/package` for that package's `.php`, or as `vendor/package:path/to/file.php`: + +```json +{ + "extra": { + "scoping-stubs": [ + "php-stubs/woocommerce-stubs", + "php-stubs/woocommerce-stubs:woocommerce-packages-stubs.php", + "./stubs/host-plugin.php" + ] + } +} +``` + +## Text domains + +A package whose strings take the host plugin's text domain declares the domain it uses, and the project declares its own: + +```json +{ + "extra": { + "scoping-text-domain": "your-library" + } +} +``` + +## Project settings and recipes + +`scoper.inc.php` returns an ordinary php-scoper config, so a project can add exclusions or patchers before returning it. Write namespace exclusions as anchored regular expressions, such as `'/^YourVendor\\\\Name(?:\\\\|$)/i'`, because php-scoper matches a plain namespace string anywhere in a name. + +A recipe returns php-scoper settings for one package, with paths relative to it: `exclude-files`, anchored `exclude-namespaces`, and `replacements`, a map of search to replace strings per file of the scoped code, in which `{prefix}` stands for the scoping prefix. A replacement whose search string is missing fails the run, so a library update that invalidates a recipe surfaces when scoping. From 3c42a3c52204da012e6747206e8c7f98ec75c97e Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:32:46 +0100 Subject: [PATCH 10/15] Name the wp-env, lint:types and conventional scripts in the contract The scripts contract says it names the scripts the workflows and shared configs read, but it left out wp-env, which the Playwright base's web server runs, so a project without it fails only a local npm run test:e2e. Its conventional lint list left out lint:types, the lane the tsconfig baseline exists for, and the README never said that the tsconfig takes effect only through a project's tsc script. The names the repositories share for recurring jobs, such as setup, packages-install, audit and quality-check, were written down nowhere. The Fixed names table lists wp-env, the conventional lint list adds lint:types, a Conventional names table gives each shared script's job, and the README says why the tsconfig needs a tsc script. Assisted-by: Claude Code:claude-opus-5-5 --- README.md | 2 ++ docs/scripts-contract.md | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6e98f4..6a0e38b 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ TypeScript extends the base by path: } ``` +The tsconfig takes effect only through the project's `tsc` script, conventionally `lint:types`, because wp-scripts builds TypeScript without type-checking it. + ESLint spreads the base: ```js diff --git a/docs/scripts-contract.md b/docs/scripts-contract.md index 5624e7b..b6bba6a 100644 --- a/docs/scripts-contract.md +++ b/docs/scripts-contract.md @@ -4,12 +4,13 @@ This document names the Composer and npm scripts and the project files that the ## Fixed names -A workflow runs these scripts by name, and no input changes the name. +A workflow or a shared config runs these scripts by name, and no input changes the name. | Script | Ecosystem | Read by | When | | --- | --- | --- | --- | | `build` | npm | `reusable-release.yml` | Runs as `npm run build --if-present` before the archive is built. | | `test:e2e` | npm | `reusable-release.yml` | Runs as `npm run test:e2e --if-present` against the built zip. | +| `wp-env` | npm | `node/playwright.config.base.js`, through the `@wordpress/scripts` Playwright config it spreads | Runs as `npm run wp-env start`, Playwright's web server, unless the project sets `webServer.command`. | ## Default names @@ -26,10 +27,30 @@ These names are the default of a `workflow_call` input; matching them avoids a ` `reusable-php-lint.yml` and `reusable-scripts-styles-lint.yml` take a required `scripts` array and run each name as its own job, so the workflows name none of them. The conventional lists are: - `reusable-php-lint.yml`: `'["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]'`; -- `reusable-scripts-styles-lint.yml`: `'["lint:scripts", "lint:styles"]'`. +- `reusable-scripts-styles-lint.yml`: `'["lint:scripts", "lint:styles", "lint:types"]'`, where `lint:types` runs `tsc`. A project that leaves out `lint:php:phpcs:tests` lints its production code but not its tests, because the production profile skips every `tests/` directory. +## Conventional names + +No workflow or shared config reads these names. A repository that has the job uses the name, so a script does the same job in every repository, and one without the job leaves the script out. + +| Script | Ecosystem | Job | +| --- | --- | --- | +| `setup` | npm | Installs every dependency and starts the development environment. | +| `packages-install`, `packages-update` | composer, npm | Install or update the dependencies; the Composer scripts pass `--ignore-platform-req=php+`. | +| `packages-install:all`, `packages-update:all` | npm | Run the Composer and npm scripts of the same stem, the update ending with `packages-update:wp`. | +| `packages-update:wp` | npm | Moves the `@wordpress/*` packages to their latest releases with `wp-scripts packages-update`. | +| `wp-env:start`, `wp-env:stop`, `wp-env:update`, `wp-env:destroy` | npm | Manage the development environment; each further environment takes `wp-env::`, such as `wp-env:tests:start`. | +| `format:php`, `format:php:tests` | composer | Fix PHP with `phpcbf` against the project's two rulesets. | +| `lint:php` | composer | Runs `lint:php:phpcs`, `lint:php:phpcs:tests` and `lint:php:phpstan`. | +| `test:unit`, `test:integration` | composer | Run one PHPUnit suite each. | +| `quality-check` | composer | Validates the manifest, and the changelog fragments where they exist, then runs the PHP lint and the tests: the check before a pull request. | +| `format:all`, `lint:all`, `test:all`, `quality-check:all` | npm | Run the Composer and npm sides of the job together, Composer first. | +| `audit`, `audit:fix` | npm | Audit both lockfiles with the supply-chain audit's default flags, so a local run and CI agree, or fix what npm can. | +| `check:engines`, `check:licenses` | npm | Check the running Node and npm against `engines`, or the dependencies' licenses. | +| `changelog:add`, `changelog:validate`, `changelog:write` | composer | Record a changelog fragment, validate the fragments, or write them into `CHANGELOG.md` for a release. | + ## PHPCS The two shared profiles run as two scripts, one over the project and one over `tests/`: From 9706cbb072d450a0784443298277d663421ac60c Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:33:11 +0100 Subject: [PATCH 11/15] Describe the Node baselines by what they do The README's tsconfig row copied every option of the file in another order, so it went stale with each option change, the ESLint row left out the console allowance the base adds, and the Stylelint example copied the base's rules into a new object for a reason that only explains the spread. Nothing said that the package aliases prettier to wp-prettier, its one runtime dependency, which decides what a consumer's wp-scripts format runs. The PHPStan floor key lacked its WPCompat parent, and the Plugin Check note said a parser "stops at" PHP 8.4 syntax, which reads as support up to it. The rows describe each baseline's intent, the Stylelint example spreads the base alone, one sentence names the alias, the floor reads WPCompat.requiresAtLeast, and the parser "fails on" the syntax. Assisted-by: Claude Code:claude-opus-5-5 --- README.md | 12 +++++++----- docs/workflows.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6a0e38b..3be4fe8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Shared PHPCS, PHPStan and php-scoper configurations, Node tool baselines and reu - PHP at the floor `composer.json`'s `require.php` names, and Composer 2. - For the Node baselines only, the Node and npm versions `package.json`'s `engines` names, and the tools its `peerDependencies` list. -The shared PHPCS profiles check code against the PHP and WordPress floors that `php/quality-assurance/phpcs.base.dist.xml` sets in `testVersion` and `minimum_wp_version`, and the PHPStan profile against the WordPress floor that `php/quality-assurance/phpstan.dist.neon` sets in `requiresAtLeast`; a project with lower floors lowers them as [the scripts contract](docs/scripts-contract.md#lowering-the-floors) shows for PHPCS and [its PHPStan section](docs/scripts-contract.md#phpstan) for PHPStan. +The shared PHPCS profiles check code against the PHP and WordPress floors that `php/quality-assurance/phpcs.base.dist.xml` sets in `testVersion` and `minimum_wp_version`, and the PHPStan profile against the WordPress floor that `php/quality-assurance/phpstan.dist.neon` sets in `WPCompat.requiresAtLeast`; a project with lower floors lowers them as [the scripts contract](docs/scripts-contract.md#lowering-the-floors) shows for PHPCS and [its PHPStan section](docs/scripts-contract.md#phpstan) for PHPStan. ## Installation @@ -178,10 +178,12 @@ return ( require __DIR__ . '/vendor/ahegyes/wordpress-configs/php/php-scoper/sco `node/` holds one baseline per tool, which a project extends and overrides. The package declares the tools behind them as `>=` peer dependencies, floored at the versions it is tested against: `@wordpress/scripts`, `@wordpress/eslint-plugin`, `@wordpress/stylelint-config`, `@playwright/test` and `typescript`, whose defaults `node/tsconfig.base.json` builds on. `typescript` stays below 7, because typescript-eslint, which the ESLint base loads, refuses TypeScript 7. +The package depends on `prettier` as an alias of `wp-prettier`, so a consumer's `wp-scripts format` runs WordPress's fork of Prettier. + | Baseline | Holds | | --- | --- | -| `node/tsconfig.base.json` | The deltas from TypeScript's defaults: `react-jsx`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, `isolatedModules`, `resolveJsonModule`, `skipLibCheck` and `noEmit` | -| `node/eslint.config.base.mjs` | `@wordpress/eslint-plugin`'s recommended flat config, with its unit-test and Playwright presets scoped to test globs | +| `node/tsconfig.base.json` | TypeScript's strict defaults, tightened for returns and switch cases and set up for code that wp-scripts bundles and `tsc` only type-checks | +| `node/eslint.config.base.mjs` | `@wordpress/eslint-plugin`'s recommended flat config, with its unit-test and Playwright presets scoped to test globs, and `console.warn` and `console.error` allowed while other console calls warn | | `node/stylelint.config.base.js` | `@wordpress/stylelint-config/scss-stylistic`, WordPress's SCSS and formatting rules, with generated trees ignored, `selector-class-pattern` off, and reports on needless, descriptionless and invalid-scope disable comments | | `node/playwright.config.base.js` | A factory over the `@wordpress/scripts` Playwright config, taking the project's wp-env `port` | @@ -204,12 +206,12 @@ import baseConfig from '@ahegyes/wordpress-configs/node/eslint.config.base.mjs'; export default [ ...baseConfig ]; ``` -Stylelint spreads the base and its rules, because Stylelint ignores the `ignoreFiles` of a config loaded through `extends`: +Stylelint spreads the base, because Stylelint ignores the `ignoreFiles` of a config loaded through `extends`: ```js const baseConfig = require( '@ahegyes/wordpress-configs/node/stylelint.config.base.js' ); -module.exports = { ...baseConfig, rules: { ...baseConfig.rules } }; +module.exports = { ...baseConfig }; ``` Playwright calls the factory with the port the wp-env config serves on (8888 when it sets none) and spreads nested keys individually, or the top-level spread drops the WordPress defaults: diff --git a/docs/workflows.md b/docs/workflows.md index c24ef3d..3c2c9b7 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -183,7 +183,7 @@ Runs WordPress Plugin Check against a built plugin directory in a fresh wp-env, | `wp-org` | `boolean` | No | `false` | Runs the full check set, for a build published to wp.org. | | `exclude-checks` | `string` | No | `''` | Comma-separated check slugs to skip beside the profile's own. | -The default GitHub profile skips `plugin_updater` and `plugin_readme`, which report two by-design traits of a GitHub-distributed build: its `Update URI` header and its missing `readme.txt`. Plugin Check skips `vendor-prefixed/`, so scoped dependencies are checked at their source. `exclude-checks` skips checks that cannot run on the plugin's code, such as `prefixing` and `direct_file_access`, which parse every PHP file with a parser that stops at PHP 8.4 syntax such as `protected(set)`. The job fails on any error, while warnings only annotate. The action installs the newest Plugin Check release from wp.org, not a pinned one. +The default GitHub profile skips `plugin_updater` and `plugin_readme`, which report two by-design traits of a GitHub-distributed build: its `Update URI` header and its missing `readme.txt`. Plugin Check skips `vendor-prefixed/`, so scoped dependencies are checked at their source. `exclude-checks` skips checks that cannot run on the plugin's code, such as `prefixing` and `direct_file_access`, which parse every PHP file with a parser that fails on PHP 8.4 syntax such as `protected(set)`. The job fails on any error, while warnings only annotate. The action installs the newest Plugin Check release from wp.org, not a pinned one. The called job needs no permissions. From 87c976ed6ff51bb785610c8f6b1f21d25f242846 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:33:33 +0100 Subject: [PATCH 12/15] Keep each contributing and security rule in one file CONTRIBUTING.md said a Node baseline change reaches consumers at their next Composer update, while npm consumers get it only when they move their pin, so contributors judged its impact by the wrong timing; the prettier alias, which changes what every consumer's formatter runs, was missing from that paragraph. The never-automerge rule for release pin bumps lived in CONTRIBUTING.md and SECURITY.md alike, the setup step used a plain composer install beside the repository's packages-install script, and SECURITY.md's scope gave vulnerability classes as examples of a component and addressed the reporter in a second voice. CONTRIBUTING.md states both timings and the alias, uses packages-install, and links to SECURITY.md for the pin-bump rule, whose scope sentence now reads as a list of components. Assisted-by: Claude Code:claude-opus-5-5 --- CONTRIBUTING.md | 6 +++--- SECURITY.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad027c2..3612252 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,13 @@ # Contributing -Consumers install `trunk` directly: Composer projects require `dev-trunk`, and workflow and npm consumers pin a `trunk` commit. Merge nothing to `trunk` that you would not release. A change to a Composer callback, a shared ruleset or a Node baseline reaches every Composer consumer at its next update, so state its consumer impact as for a workflow input. +Consumers install `trunk` directly: Composer projects require `dev-trunk`, and workflow and npm consumers pin a `trunk` commit. Merge nothing to `trunk` that you would not release. A change to a Composer callback or a shared ruleset reaches every Composer consumer at its next update, and a change to a Node baseline or to the package's `prettier` alias of `wp-prettier` reaches npm consumers when they move their pin; state its consumer impact as for a workflow input. ## Before opening a pull request Install the dependencies on the PHP, Composer, Node and npm versions the README lists, then run the local equivalents of the CI checks: ```sh -composer install +composer packages-install npm install composer quality-check composer audit --abandoned=report @@ -24,7 +24,7 @@ zizmor --config .github/zizmor.yml .github/ Shipped `workflow_call` input names, types and semantics are frozen. An input may be added only when it is optional and its default keeps the current behavior. Renaming, removing or re-typing an input, or changing a default in a way callers can see, is a coordinated migration: land the change and move every consumer's pin in the same window, never by automerge, because a caller passing an input the workflow no longer declares fails at startup without a failed check on the pull request that moved the pin. -Give every input a `description:` in the workflow and a row in [docs/workflows.md](docs/workflows.md). Consumers review, never automerge, a pin bump that changes the release workflow's `release` or `wp-org` jobs. +Give every input a `description:` in the workflow and a row in [docs/workflows.md](docs/workflows.md). [SECURITY.md](SECURITY.md#release-workflow) sets how consumers review a pin bump that changes the release workflow. Dependabot cannot see the actionlint `version` and the `ZIZMOR_VERSION` that `reusable-workflow-checks.yml` pins, so bump them by hand to the newest release each pinned action supports. diff --git a/SECURITY.md b/SECURITY.md index c675ddc..d4e18a6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a vulnerability -Report a vulnerability privately through [GitHub's security advisory form](https://github.com/ahegyes/wordpress-configs/security/advisories/new), never in a public issue. Expect a first response within 7 days. Once a fix lands on `trunk`, the advisory credits the reporter, unless they ask to stay anonymous. +Report a vulnerability privately through [GitHub's security advisory form](https://github.com/ahegyes/wordpress-configs/security/advisories/new), never in a public issue. The first response comes within 7 days. Once a fix lands on `trunk`, the advisory credits the reporter, unless they ask to stay anonymous. ## Supported version @@ -14,6 +14,6 @@ Pin `reusable-release.yml` by commit SHA like every reusable workflow, and revie ## Scope -In scope is everything this repository ships: the scoping pipeline, its php-scoper configuration and recipes, the shared PHPCS and PHPStan configurations, the reusable workflows, such as script injection through caller inputs or an unpinned third-party action, and the Node baselines. +In scope is everything this repository ships: the scoping pipeline, its php-scoper configuration and recipes, the shared PHPCS and PHPStan configurations, the reusable workflows (including script injection through caller inputs and unpinned third-party actions) and the Node baselines. Out of scope are vulnerabilities in WordPress core, third-party Composer or npm packages, and third-party GitHub Actions; report those to their maintainers. From 2195adad07af3554f1bb2d69476702edd8523234 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:34:11 +0100 Subject: [PATCH 13/15] Number the repository conventions and split out the style rules AGENTS.md held thirteen unnumbered conventions without bold leads, against the shape the other repositories' agent files follow, with the docblock, comment, Markdown and commit rules mixed in among the repository's own. Its naming rule claimed every shared config is named .dist., which only the PHPCS and PHPStan profiles are, its floor rule named the PHPStan key without its file and WPCompat parent, and its first command was a plain composer install beside the repository's packages-install script. The nine repository conventions are numbered with bold leads, the four style rules sit in a Style section before Limitations, the naming rule covers both kinds of shared file, the floor names its file and full key, and the commands start with packages-install. Assisted-by: Claude Code:claude-opus-5-5 --- AGENTS.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dbb728e..0d5d134 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ wordpress-configs/ ## Commands ```sh -composer install +composer packages-install composer quality-check # composer validate --strict, PHPCS (both profiles), PHPStan, unit tests composer test:unit:mutation # opt-in Infection run, no floor npm run lint:scripts # ESLint over node/, the smoke harness and eslint.config.mjs @@ -39,15 +39,18 @@ zizmor --config .github/zizmor.yml .github/ ## Conventions -- Shared config files are named `.dist.`, so IDEs recognize the trailing extension. -- Self-lint uses the root rulesets and PHPStan config, which extend the shared profiles with this repository's exclusions; the files under `php/quality-assurance/` are for consumers. -- Composer installs pass `--ignore-platform-req=php+` in scripts, CI and the reusable workflows: only the PHP upper bound is ignored, so extension requirements and floors stay checked. -- Tests run PHPCS, PHPStan, php-scoper and workflow step scripts on fixtures copied to a temporary directory, never mocks. -- Workflow-step tests find a step by its job and `name` and run it with `bash -e`, as GitHub Actions does for a step that names no shell; renaming a tested step breaks its test. -- Every reusable-workflow input has a `description:`; shipped input names, types and defaults change only as a coordinated migration (CONTRIBUTING.md). -- Self-CI calls the reusable workflows through `./`, and `.github/zizmor.yml` turns off zizmor's `self-repository` audit until actionlint accepts `$/`. -- The README is an index; workflow inputs live in `docs/workflows.md`, script and file contracts in `docs/scripts-contract.md`, the scoping contract in `docs/scoping.md`, test tiers in `tests/README.md`. A behavior change updates them in the same change. -- The README names each floor by the file and key that hold it (`composer.json`'s `require.php`, `package.json`'s `engines`, `phpcs.base.dist.xml`'s `testVersion` and `minimum_wp_version`, `phpstan.dist.neon`'s `requiresAtLeast`), never by its number, so a floor change edits only those files. +1. **Naming.** Shared PHP tool configs, the PHPCS and PHPStan profiles, are named `.dist.`, so IDEs recognize the trailing extension, and the Node baselines `.config.base.`, or `tsconfig.base.json` for TypeScript. +2. **Self-lint.** This repository lints itself with the root rulesets and PHPStan config, which extend the shared profiles with its exclusions; the files under `php/quality-assurance/` are for consumers. +3. **Composer installs.** Scripts, CI and the reusable workflows install with `--ignore-platform-req=php+`: only the PHP upper bound is ignored, so extension requirements and floors stay checked. +4. **Tests.** PHPCS, PHPStan, php-scoper and workflow step scripts run on fixtures copied to a temporary directory, never against mocks. +5. **Workflow-step tests.** A test finds a step by its job and `name` and runs it with `bash -e`, as GitHub Actions does for a step that names no shell; renaming a tested step breaks its test. +6. **Inputs.** Every reusable-workflow input has a `description:`; shipped input names, types and defaults change only as a coordinated migration (CONTRIBUTING.md). +7. **Self-CI.** This repository's CI calls the reusable workflows through `./`, and `.github/zizmor.yml` turns off zizmor's `self-repository` audit until actionlint accepts `$/`. +8. **Docs.** The README is an index; workflow inputs live in `docs/workflows.md`, script and file contracts in `docs/scripts-contract.md`, the scoping contract in `docs/scoping.md`, test tiers in `tests/README.md`. A behavior change updates them in the same change. +9. **Floors.** The README names each floor by the file and key that hold it (`composer.json`'s `require.php`, `package.json`'s `engines`, `phpcs.base.dist.xml`'s `testVersion` and `minimum_wp_version`, `php/quality-assurance/phpstan.dist.neon`'s `WPCompat.requiresAtLeast`), never by its number, so a floor change edits only those files. + +## Style + - Class and method docblocks open with one third-person sentence naming the role, such as "Returns …", and add a second paragraph only for a non-obvious why; property and constant docblocks open with "The …". A function or method docblock ends with `@return`, `@return void` included, unless it documents a constructor or opens with `{@inheritDoc}`. Each `@param` stays on one line, `@throws` reads "Thrown when …", members are `protected` rather than `private`, and class bodies outside `tests/` group members in `// region NAME` blocks. - Inline comments state a non-obvious why in one capitalized sentence, and every file describes the present, never the change. - Markdown keeps one line per paragraph, sentence-case headings and an impersonal, present-tense voice. From edc6da01c130b387927654704f5a983c47533d12 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:34:20 +0100 Subject: [PATCH 14/15] Point the pull request checklist at CONTRIBUTING's checks The template asked only that composer test pass locally, while CONTRIBUTING.md lists the full set of local checks CI mirrors: the quality check, the Node lints and smoke, both audits, actionlint and zizmor. A contributor who ticked the box could still fail CI on everything the unit suite leaves out. Assisted-by: Claude Code:claude-opus-5-5 --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e652852..5445b8f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,7 +14,7 @@ ## Checklist - [ ] Tests added or updated (PHPUnit suites under `tests/Unit/`) -- [ ] `composer test` passes locally +- [ ] The checks in CONTRIBUTING.md pass locally - [ ] If this changes a reusable workflow's inputs/outputs, `docs/workflows.md` is updated to reflect the new contract - [ ] If this changes a ruleset, downstream consumer behavior is considered (this repo ships to `dev-trunk` consumers immediately) From 0280692cf1db4f262bcafe0a9e64cc12f3a3c326 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sat, 7 Nov 2026 10:34:29 +0100 Subject: [PATCH 15/15] Say why the package exists in its description composer.json and the README opened with two different lists of what the package holds, PHPCS, PHPStan and php-scoper configurations, Node baselines and workflows, which grow with every feature and differed from each other and from GitHub's description. Both now carry the sentence GitHub already shows: shared tooling that lets WordPress projects be checked, scoped and released the same way. Assisted-by: Claude Code:claude-opus-5-5 --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3be4fe8..3684dc5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # wordpress-configs -Shared PHPCS, PHPStan and php-scoper configurations, Node tool baselines and reusable GitHub Actions workflows for WordPress plugins, themes and sites. +Shared tooling that lets WordPress projects be checked, scoped and released the same way. ## Requirements diff --git a/composer.json b/composer.json index 8bfef80..e3329c8 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "ahegyes/wordpress-configs", "type": "library", - "description": "Shared PHPCS, PHPStan and php-scoper configurations and reusable GitHub Actions workflows for WordPress projects.", + "description": "Shared tooling that lets WordPress projects be checked, scoped and released the same way.", "license": "MIT", "authors": [ {