Repository navigation
Close the reusable workflows' release, audit and check gaps - #62
Merged
Merged
Conversation
The locks trailed the latest releases within range. The root Composer lock needs nothing; the release tools' lock gains Composer 2.11's published-time metadata. npm moves eslint (under wp-scripts) to 10.14.0 and minimizer-webpack-plugin to 5.14.0; TypeScript stays on 6, which typescript-eslint caps, and the cooldown's downgrade of @babel/helper-create-regexp-features-plugin to 7.29.7 is kept back at 7.29.10. Workflow Checks' zizmor pin, which Dependabot cannot bump, trailed as well; it moves to 1.31.1, the newest release the pinned zizmor-action supports. Assisted-by: Claude Code:claude-opus-5-5
Rows for the behavior the reusable workflows lack, each failing on trunk: - release: prerelease tags and their versions, notes and the wp.org refusal; the WooCommerce floor from extra.requires-wc; Composer's configured vendor directory; the forbidden development files and private keys under vendor/; every scoped PHP file in the archive; the changelog section checked in the build and handed to the release; the wp-env override merged, with core cleared per environment, and the site requested on the effective port; - the end-to-end workflow resolving the site's PHP with the PHPUnit workflow's step, and naming a missing @playwright/test; - Composer's cooldown findings read as findings; - Workflow Checks' extra-workflows list for actionlint; - the GHAS probe in reusable CodeQL, which runs analyze on an unclear answer; - the block.json check validating with the ajv-cli node/block-json/ locks. npm audit rows pin today's advisory handling before its rewrite. run_step() reads multi-line `name<<delimiter` values, which the release notes output uses. Assisted-by: Claude Code:claude-opus-5-5
The bootstrap updater has a prerelease channel that scans a plugin's newest releases for dev, alpha, beta and rc versions, but the release workflow refused every tag except vMAJOR.MINOR.PATCH, so nothing could produce one. A tag may add -dev, -alpha, -beta or -rc and an optional .N. Its version must still equal the main file's Version header, but not readme.txt's Stable tag, which keeps naming the last stable release. The release is created with --prerelease, which GitHub's latest release skips, and the wp.org deploy never runs for it. Assisted-by: Claude Code:claude-opus-5-5
The release job alone looked up the version's CHANGELOG.md section, so a dry run passed what the tagged run then failed, and a section holding only blank lines published an empty description. The build extracts the section beside the version check, fails when it is missing or blank, and hands it to the release job as the multi-line release-notes output, so the tagged run publishes exactly the notes the dry run checked. The release job then reads nothing from the tree and drops its checkout. Assisted-by: Claude Code:claude-opus-5-5
The release gate checked Requires PHP and Requires at least against the shipped packages but no WooCommerce floor, so a plugin could declare a WC requires at least below what a bundled package needs. It also read production packages only from vendor/composer/installed.json, so a project with a custom Composer vendor-dir skipped the PHP-floor check for every production package. A shipped package's extra.requires-wc now sets a WC requires at least floor, as extra.requires-wp sets Requires at least, and the installed packages come from the vendor directory composer config reports. Assisted-by: Claude Code:claude-opus-5-5
The completeness check passed a scoped package as long as any one of its files reached the zip, so a .distignore pattern that dropped its code while its composer.json or README shipped went unnoticed until the plugin fataled. Every PHP file under vendor-prefixed/ must now ship, scoper-autoload.php included, while a package's metadata may stay out; the template, Linked Orders, Internal Comments and Locked Payment Methods archives pass it. The forbidden-entry check let profile PHPStan configs, PHPUnit caches, AGENTS.md, CLAUDE.md and changelog/ fragments through, and exempted .key files under vendor/ with the certificates. Those files now fail the build, and only .pem certificates under vendor/ and vendor-prefixed/ stay exempt. Assisted-by: Claude Code:claude-opus-5-5
The artifact test wrote the config's override file from scratch, so any override the project kept, such as its own constants or a co-mounted plugin, was dropped, and it cleared core only at the root, so a core pinned for the development or tests environment still ran the E2E against that pin. The site check also read the port from the config alone. The redirect and core: null now merge into the existing override at the root and in both environments, and the site is requested on the development port the config and its override set. docs/workflows.md also says that a newer tag cancels an older one's pending release, which is re-run from its tag's run. Assisted-by: Claude Code:claude-opus-5-5
A release attached its zip to the GitHub release with nothing that ties it to the workflow run that built it, so a downloaded copy could not be checked against its build. The release job attests the verified zip with actions/attest-build-provenance before it creates the release, which gh attestation verify checks. GitHub offers attestations in private and internal repositories only on Enterprise Cloud, so the step runs only in a public repository and a private one still releases. Callers grant id-token: write and attestations: write as well. Assisted-by: Claude Code:claude-opus-5-5
The Playwright workflow exported php-version as WP_ENV_PHP_VERSION for the whole job, which wp-env applies over every phpVersion, so a suite aimed at a site pinned to an older PHP ran on php-version instead, while the PHPUnit workflow keeps the pin. The end-to-end workflow drops the job-level variable and runs the PHPUnit workflow's "Resolve the site's PHP" step verbatim, which a test holds identical. A project without @playwright/test now fails with a named error instead of an empty version, and the browser install runs npx with --no-install, as the release does. Assisted-by: Claude Code:claude-opus-5-5
Composer 2.11 fails an audit on a package still in policy.cooldown's window when that policy says fail, and reports it under a cooldown key the classifier never read, so such a run was read as a broken report and failed even with fail-on-findings off. Cooldown entries now count as findings beside advisories and abandoned packages, and docs/workflows.md says what the classifier does: any filter-list entry fails the audit. npm-audit-flags defaulted to the full dependency graph, which every caller overrode with --omit=dev --audit-level=high, the gate this project runs. That is now the default, and configs' own caller drops its override. Assisted-by: Claude Code:claude-opus-5-5
A monorepo whose packages ship their own workflows under plugins/*/.github/workflows had no way to lint them: actionlint reads only the root's .github/workflows. extra-workflows, an optional JSON array of globs, now joins those files to actionlint's list. zizmor needs nothing, because its default input already audits every workflow in the repository and skips gitignored trees. actionlint ran whatever release the action fetched, beside a pinned zizmor; it is now pinned to 1.7.12, and CONTRIBUTING names both pins as manual bumps, since Dependabot sees neither. configs' own Workflow Checks also runs when zizmor.yml or dependabot.yml changes. Assisted-by: Claude Code:claude-opus-5-5
Every consumer's release installs WP-CLI and its commands from php/release/composer.lock, but the supply-chain audit read only the root's locks, so an advisory in that tree never failed configs' CI. Quality now audits it as well, with the npm audit off, since it holds no npm manifest. Assisted-by: Claude Code:claude-opus-5-5
The block.json check ran `npx --yes ajv-cli@5.0.0`, which fixes only the top-level version, so every consumer's CI resolved ajv-cli's dependency tree fresh from the registry, unlocked, uncooled and unseen by Dependabot. node/block-json/ now holds the manifest and lock of that ajv-cli, as php/release/ does for the release's WP-CLI commands. The job checks it out from the workflow's own commit through job.workflow_repository and job.workflow_sha, installs it with npm ci and calls its ajv. Dependabot keeps the lock current, Quality audits it, and the npm package leaves the directory out. ajv-cli's last release requires fast-json-patch 2, whose prototype-pollution advisory is high, so an override holds it at 3.1.1, under which ajv validates the fixture and rejects an invalid block as before. actionlint 1.7.12 does not know the two job.workflow_* properties yet, so .github/actionlint.yaml ignores those two messages for that file. Assisted-by: Claude Code:claude-opus-5-5
On a private repository without GitHub Advanced Security, CodeQL's analyze step fails on its SARIF upload, so reusable CodeQL failed every run there, while Workflow Checks already probes for it. CodeQL now runs a8csp-configs' probe first and skips analyze when the code-scanning API answers that Advanced Security is off. An answer that stays unclear after three tries counts as available, unlike Workflow Checks' probe, because a skipped analyze would pass as a green check while zizmor still runs and fails on findings there. Assisted-by: Claude Code:claude-opus-5-5
The syntax check defaulted to PHP 8.5 alone, so a construct the next PHP deprecates or removes surfaced only when a project raised its PHP. The default now also runs 8.6, which setup-php installs as a nightly build, as a8csp-configs does. Assisted-by: Claude Code:claude-opus-5-5
The scoping pipeline relies on php-scoper 0.18's behavior but declared the range only as its own development requirement, which a consumer's install never sees, so a consumer could scope with another minor. A conflict now holds php-scoper to 0.18, and the README says so. The pipeline also reads jetbrains/phpstorm-stubs directly, which php-scoper 0.18 installs, without naming it; suggest names it beside php-scoper. The composer/* plugin allowance, which matched no installed plugin, goes. Assisted-by: Claude Code:claude-opus-5-5
The release workflow said several things twice or at length: two helper functions behind one project-path check, eight case arms sharing one body, a GITHUB_ENV copy of a step output, a second Node-version check on the commit the build had checked, and Composer installs with differing flags. Each now says it once, and every check keeps its outcome: - the release input checks on project-path are one condition with one message, and a row pins its character-pattern branch; - the eight forbidden-entry arms that shared one body are one arm, the certificate and secret arms keeping their order; - the POT check reads the scoping step's output in its condition, so the job no longer copies that fact into GITHUB_ENV; - the wp.org job takes its version from the build's output and keeps its own tag check, with a line saying why; - the test job drops the Node-version check the build already ran on the same commit; - the release's Composer installs share one flag set. Assisted-by: Claude Code:claude-opus-5-5
The release's floor check and the Composer audit's classifier were written in a compact style the repository's PHP does not use: [] arrays, no spaces inside parentheses, and array_filter and array_map closures where a loop reads plainer. Both now use array(), WPCS spacing, Yoda comparisons against preg_match() and a foreach for the floor messages, and the classifier moves from php -r into a heredoc, so its strings take single quotes. Messages and exit statuses are unchanged; extracted, both scripts pass WPCS but for the short ternary configs' base allows and the WordPress-globals sniff, which a CI script never meets. Assisted-by: Claude Code:claude-opus-5-5
Both audit steps toggled errexit around the audit to capture its status, and the npm step classified its report with a Node heredoc chained to an error block. Each step now captures the status with `|| status=$?`, and the npm report is classified by one jq expression with the same outcomes: findings in a readable report are softened, while an error report or an unreadable one fails. Quality's PHPCS smoke drops a `set -e -o pipefail` that its `shell: bash` already applies. Assisted-by: Claude Code:claude-opus-5-5
The release's archive check gained AGENTS.md, CLAUDE.md, changelog/, every phpstan*.neon* and .phpstan*.neon* file, PHPUnit's result cache and any .key file under vendor/, but the baseline consumers copy excluded none of them, so a project built from the baseline failed its release on a file the baseline let through, such as PHPStan's own phpstan.neon.dist. The baseline now excludes each of them, and *.key at any depth, as the release requires; .pem stays root-anchored, so certificate bundles under vendor/ still ship. A wp dist-archive build of a probe plugin from the baseline passes the release's check. Assisted-by: Claude Code:claude-opus-5-5
The site check's port lookup lost jq's -r flag when it began merging the override, so a port written as a string would reach curl in quotes. The wp.org job's comment also said it trusts nothing the earlier jobs found, though it takes its version from the build; it now says what its ref check guards. Assisted-by: Claude Code:claude-opus-5-5
A port test ran a statement straight after a closing brace, and the CodeQL test explained its curl fake in a comment its sibling fakes do without. Assisted-by: Claude Code:claude-opus-5-5
A prerelease leaves readme.txt's Stable tag at the last stable release, but with wp-org on the build runs Plugin Check's readme check, which reports a Stable tag that differs from the header's Version as the stable_tag_mismatch error, so every prerelease of a wp.org plugin failed its build. The build now passes that code to Plugin Check's ignore list when the header's version is a prerelease. Assisted-by: Claude Code:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The release, supply-chain, end-to-end, block.json, CodeQL and workflow-check workflows let broken releases, unlocked tools and misread reports through; this fixes each case, adds prerelease tags and build provenance attestations, and simplifies the release and audit shell.
Type of change
.github/workflows/reusable-*.yml)php/quality-assurance/)Checklist
tests/Unit/)composer testpasses locallydocs/workflows.mdis updated to reflect the new contractdev-trunkconsumers immediately)Breaking changes for downstream consumers
Move each consumer's pin in one window, since a caller missing a permission fails at startup without a failed check on the pull request that moved the pin.
reusable-release.ymlgrantid-token: writeandattestations: writebesideactions: readandcontents: write. In a public repository the release attests its zip's build provenance; a private or internal one releases without an attestation.npm-audit-flagsdefaults to--omit=dev --audit-level=high, so callers passing that value can drop it, and a caller that audits development dependencies passes--audit-level=high.reusable-php-syntax-check.ymlchecks PHP 8.5 and 8.6 (a nightly build) by default.reusable-playwright-e2e.ymlexportsWP_ENV_PHP_VERSIONonly when the wp-env config and its override pin nophpVersion, as the PHPUnit workflow does.CHANGELOG.md, every run, dry runs included, needs a non-blank section for the header's version, which becomes the release notes.WC requires at leastmust cover each shipped package'sextra.requires-wc, and the floors read Composer's configured vendor directory. The archive must ship every PHP file undervendor-prefixed/and must not shipAGENTS.md,CLAUDE.md,changelog/, PHPStan configs, PHPUnit caches or any.keyfile; the baseline.distignoreexcludes them all, so copy it again..override.jsonrather than replacing it, clearscorein each environment, and requests the site on the development port the config and its override set.humbug/php-scoperoutside 0.18.Additive:
v1.2.3-beta.1-style tags publish GitHub prereleases and skip wp.org;reusable-workflow-checks.ymltakes an optionalextra-workflowslist for actionlint and pins actionlint; CodeQL skips analysis where GitHub Advanced Security is off instead of failing; the block.json check validates with a locked ajv-cli from this repository's commit; Composer cooldown findings followfail-on-findings.