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/.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) 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 465fa14..043b702 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,9 +41,8 @@ jobs: paths: '["tests/Fixtures/build-fixture"]' phpcs-smoke: - name: PHPCS dist-file smoke - # 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. + name: Smoke-test the PHPCS 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: @@ -53,7 +52,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' @@ -65,12 +64,11 @@ 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 - - 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 +86,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 +101,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,8 +110,8 @@ jobs: composer-audit: false block-json: - name: block.json schema check - # Dogfoods reusable-block-json-check against a throwaway fixture; this tooling repo ships no real blocks. + name: Check block.json files + # This repository ships no blocks, so the check runs against a fixture. permissions: contents: read uses: ./.github/workflows/reusable-block-json-check.yml @@ -121,7 +119,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 +138,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..e26a265 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: '.' @@ -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' @@ -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 d2f5321..9dbc1ee 100644 --- a/.github/workflows/reusable-codeql.yml +++ b/.github/workflows/reusable-codeql.yml @@ -4,8 +4,8 @@ on: workflow_call: 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' + # CodeQL has no PHP analyzer, so PHPStan covers PHP. + description: 'Non-empty JSON array of CodeQL languages.' type: string required: false default: '["actions"]' @@ -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 }} @@ -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..a157730 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: '.' @@ -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 }} @@ -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..e56e94b 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"]' @@ -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 }} @@ -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..4386969 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: '' @@ -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: @@ -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 }} @@ -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..3a285ab 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' @@ -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,24 +110,24 @@ 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 - # 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: @@ -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..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: @@ -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 @@ -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 }} @@ -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 }} @@ -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 @@ -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 } @@ -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: | @@ -288,8 +288,8 @@ jobs: exit 1 fi - # --no-scripts avoids invoking handlers that were just pruned from require-dev. - - name: Prune to production dependencies + # 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 @@ -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: | @@ -523,9 +523,10 @@ 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 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 +535,7 @@ jobs: retention-days: 30 test: - name: Test built artifact + name: Test the built artifact needs: build runs-on: ubuntu-latest timeout-minutes: 45 @@ -543,20 +544,20 @@ 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 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 +567,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 +596,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 }} @@ -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" @@ -663,7 +664,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,8 +673,8 @@ jobs: npm run test:e2e --if-present provenance: - name: Verify 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. + name: Verify the release provenance + # 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 @@ -681,7 +682,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 +716,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 +728,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: | @@ -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 @@ -782,19 +783,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 +806,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 +821,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..8d5c6d1 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: '.' @@ -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 }} @@ -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..a7db8a6 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 @@ -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 }} @@ -75,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" @@ -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..3111c96 100644 --- a/.github/workflows/reusable-workflow-checks.yml +++ b/.github/workflows/reusable-workflow-checks.yml @@ -13,18 +13,17 @@ 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 - # 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: @@ -92,7 +91,7 @@ jobs: done zizmor: - name: Zizmor + name: Run zizmor needs: detect-ghas runs-on: ubuntu-latest timeout-minutes: 30 @@ -101,12 +100,10 @@ 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: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -141,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." 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: diff --git a/AGENTS.md b/AGENTS.md index 12e0875..0d5d134 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 @@ -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`, 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. 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/README.md b/README.md index c3689a4..3684dc5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # 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 - 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 @@ -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" + } } ``` @@ -170,64 +170,20 @@ 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. +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` | @@ -235,11 +191,13 @@ 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/**/*"] } ``` +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 @@ -248,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: @@ -265,7 +223,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 +253,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/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. 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": [ { 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/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. diff --git a/docs/scripts-contract.md b/docs/scripts-contract.md index 7296e88..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,22 +27,42 @@ 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/`: ```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 +79,8 @@ Include a floors ruleset after the shared profile. A `phpcs.floors.xml` holds th ```xml - - + + ``` @@ -68,8 +89,8 @@ The project ruleset includes it after the profile: ```xml - - + + ``` @@ -87,9 +108,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 +136,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"] + } } ``` diff --git a/docs/workflows.md b/docs/workflows.md index 6d956ec..3c2c9b7 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`. | @@ -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. 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/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" }, 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"; } } 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 @@ 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,