Repository navigation
v1.0.0
First tagged release. Consumers pin @v1.
Changed
-
Every reusable workflow takes its runner from a new
runs-oninput (JSON: a label or a list of labels), defaulting to GitHub-hosted"ubuntu-latest", andwp-ci.ymlandwp-cd.ymlforward it to every workflow they call. Hard-coding[self-hosted]left public callers (and this public repository's own CI) queued with no runner, since a reusable workflow's jobs run in the calling repository. rtCamp's private repositories pass'["self-hosted"]', as the org runner policy requires.ci-self-check.ymlruns onubuntu-latest, andbin/check-workflows.shnow requires reusable workflows to use and forward the input instead of checking a label allow-list. -
Caller values no longer reach a
run:body through${{ }}interpolation — a conventionAGENTS.mdalready documented but sixteen steps across eight workflows broke. Value inputs (composer-flags,phpstan-level) now travel through stepenv:and become argv, so shell metacharacters in them stay literal instead of executing. Command inputs (build-command,test-command,phpcs-command,phpstan-command,lint-command, andcd-built-branch.yml's per-target equivalents) also travel throughenv:and are run asbash -eo pipefail -c "$COMMAND": executing them is the contract, but routing them through the environment stops render-time splicing, where a newline in the value injects extra lines into the generated script.-eo pipefailis not optional — a barebash -cdoes not inheritset -e, so a failing command would exit 0 and pass a job green.bin/check-workflows.shnow fails on any${{ inputs. }}/${{ matrix. }}inside arun:body, so the convention is verified rather than merely documented. -
cd-github-release.ymlpackages the release asset instead of attaching the artifact's top-level entries directly.gh release createonly accepts files, butdownload-artifactunpacks a directory artifact, so any plugin with a subdirectory — including the shapeexamples/cd-github-release.ymlproduces — failed at the upload. A directory artifact is now re-rooted under<slug>/and zipped to a single<slug>.zipthat installs through wp-admin; an artifact that is already a single archive is attached untouched, so a pre-zippedartifact-pathkeeps working. The two cases are told apart by archive extension rather than file count, so a single-file plugin still gets packaged. Adds an optionalsluginput (defaults toartifact-name). -
ci-test-js.yml'senable-cachenow caches something. Jest has noJEST_CACHE_DIRECTORYenvironment variable, so the previous wiring cached an always-empty directory, and the fixedjest-<os>key was written once and never updated again. The cache directory is now passed as a real--cacheDirectoryargument and the key includes theworking-dir-scoped lockfile hash with arestore-keysfallback. A customtest-commandmust forward its arguments for the flag to land; the default does. -
ci-self-check.ymlrunsyamllintover the whole repository rather than just.github/workflows/andexamples/, matching theyamllint .thatAGENTS.mdtells contributors to run — previously.yamllint.ymland.github/actionlint.yamlcould fail locally while CI stayed green. -
cd-built-branch.yml'spublishedandpublished-shaoutput descriptions now cover every no-push outcome. A run superseded by a newer one also reportspublished=falsewith an empty SHA, sofalsenever meant "dry run" on its own. The job summary distinguishes the two cases as well, instead of reporting a superseded run as "Publish did not complete". -
AGENTS.mdandCONTRIBUTING.mddescribebin/as Bash, not POSIX shell —bin/check-workflows.shusesBASH_SOURCE,[[ ]], arrays and process substitution, so the POSIX label gave contributors the wrong portability contract. -
Documentation restructured for a public
main. Caller examples move out ofREADME.mdintoexamples/, one runnable file per workflow, verified in CI bybin/check-workflows.sh.README.mdis now a workflow index plus a versioning guide, and no longer duplicates the input tables — each workflow'sworkflow_callblock is the single source of truth, and GitHub renders those descriptions when a caller is configured. Repo conventions move to a tool-agnosticAGENTS.md, whichCLAUDE.mdimports. AddsLICENSE(GPL-2.0-or-later, previously declared but absent) andCONTRIBUTING.md. -
version-monitor.ymlnow pinsactions/checkoutandactions/setup-nodeto the same SHAs as the other fifteen workflows, and its job carries an explicitname:. -
The
wp-tooling-tokensecret is removed fromci-detect-changes.yml,version-monitor.ymlandwp-ci.yml.rtCamp/wp-toolingis public, so the CLI now installs straight from itsnpm/wp-toolingbranch with no token. This secret never appeared in a tagged release, so v1.0.0 simply ships without it — no major bump is owed. Anyone pinned to a branch ref for pre-release smoke testing must still delete the line, or the call fails withInvalid input, wp-tooling-token is not defined in the referenced workflow. A newwp-tooling-refinput (forwarded throughwp-ci.yml) pins the install to a commit SHA and installs with--ignore-scripts, so a change on that branch cannot alter what your CI runs; override it to track the tip. The package is still not on the npm registry, so the install remains a git ref. -
The
packages-tokensecret is removed fromci-lint-php.yml,ci-test-php.ymlandwp-ci.yml.rtCamp/wp-frameworkis public, so no known private Composer dependency is left to authenticate. It was added in6274f32and removed here, both within the unreleased v1.0.0 cycle, so no tagged release ever accepted it. Anyone pinned to a branch ref must delete the line, or the call fails withInvalid input, packages-token is not defined in the referenced workflow.setup-phpstill receivesgithub-token: ${{ github.token }}for the API rate limit, which never depended on the secret, andci-test-php.ymldrops the wp-env Composer-auth step that existed only to forward it into the container. -
Monorepo fixes across the CI leaves. All six
setup-nodesteps that usecache: npmnow passcache-dependency-path, andci-lint-php.yml's Composer cache key is scoped to<working-dir>/composer.lock.ci-build-artifact-gate.ymlnormalises and regex-escapesworking-dirbefore anchoring its pattern — previously a./-prefixed or metacharacter-containing value matched nothing, so the gate passed green on a PR that committed build output.ci-test-build-artifact.ymlgainsworking-dir. -
cd-github-release.ymlandcd-wp-org.ymlgainworking-diras a path prefix, not a working directory: it scopes only the source-tree paths they read (changelog-path,assets-path), since the deployed files come from an already unit-scoped artifact. Both previously fell back to the repo root in a monorepo and failed silently — another unit'sCHANGELOG.mdstill parses, and a missing.wordpress-orgonly emits a notice. Both now reject absolute and..paths.cd-s3.ymlis unchanged. -
cd-github-release.yml,cd-wp-org.ymlandcd-s3.ymlfail fast in a first validation step when a required input arrives empty:tagandartifact-nameeverywhere, plusslugfor wporg andbucketfor s3.required: truedoes not reject an empty string, and an emptyartifact-namemakesdownload-artifactfetch every artifact in the run; an emptyslugwould fall back to the deploy action's repo-name default and could deploy to the wrong wordpress.org plugin, and an emptytagwould upload to<prefix>.zip.cd-wp-org.ymlandcd-s3.ymlalso presence-check their secrets as booleans, since a caller mapping an unset secret satisfies GitHub's required-secret check with an empty value. -
Every job runs on
runs-on: [self-hosted], per rtCamp's policy for private repositories. A reusable workflow's jobs run in the calling repository, so the caller needs access to the org's self-hosted runner group.bin/check-workflows.shapplies the same label allow-list as the org's runner-policy scanner, which stops checking a repository once it is public. These runners do not ship the AWS CLI, socd-s3.ymlinstalls a pinned, checksum-verified one when the runner has none. -
ci-detect-changes.yml, and sowp-ci.yml, now fetch an explicitbase-refbefore diffing. The CLI diffs an explicit base as-is, and the depth-2 checkout never holds a branch such asmain, so the diff failed, the CLI reported zero changes, and every downstream job skipped while the run passed green. The step also fails when the CLI reports a failed diff in any mode, instead of passing with nothing checked. -
ci-test-build-artifact.ymlpinswp-cli/doctor-commandto 2.3.1.@stableresolved to v3.0.0 (released 2026-08-04), which requires wp-cli ^3.0, while the wp-env cli container ships 2.12, so the job failed at the package install for every caller. -
AGENTS.mdandCONTRIBUTING.mdnow base task branches and pull requests onmain, addpipxto the install line becausepipx run yamllintneeds it, and list Docker Compose v2 as missing on the self-hosted runners.
Added
cd-built-branch.yml: reusable workflow that builds a project and force-pushes source plus generated output (Composervendor/, compiledassets/build/) to a dedicated branch, for platforms that deploy from a git branch rather than an artifact. Three jobs:discoverresolves thetargetsJSON into a build matrix,buildbuilds each target in parallel, andpublishforce-pushes a single commit whose parent is the source commit — sogit diff <source>..<target>is exactly the generated files, and rollback is a dispatch withsource-refplusallow-rewind: true. Every run republishes every target, so the branch is always a complete tree; a target whosediris missing fails the run unless marked"optional": true. A push-triggered run that loses a race ends green with a::notice::because the newer tree is already live, while an explicitsource-refpointing backwards errors unlessallow-rewind: true. Onlypublishholdscontents: write, so the job that executes third-party dependency code never has a writable token; the caller must grantpermissions: contents: write. See the README for the full contract and caveats. Inputs:targets,target-branch,source-ref,publish-paths,verify-paths,node-version,php-version,composer-flags,build-command,commit-subject,allow-rewind,dry-run,retention-days. Optional secret:push-token, for when a push must trigger further workflow runs, which the defaultGITHUB_TOKENnever does. Outputs:published,published-sha,source-sha,targets.ci-test-php.yml: reusable PHPUnit workflow with ause-wp-envtoggle (defaulttrue) for WordPress integration tests via@wordpress/env, or standalone PHPUnit for Composer-only packages and libraries. In wp-env mode,php-versionis pinned into the container viaWP_ENV_PHP_VERSIONandwp-version(when non-empty) setsWP_ENV_CORE=WordPress/WordPress#<version>(both set at job scope, not$GITHUB_ENV, so a caller's matrix can drive the PHP and WordPress core version per cell without newline-injection risk); an emptywp-versionuses the consumer's.wp-env.json. Inputs:php-version,node-version,wp-version,use-wp-env,test-command,composer-flags,working-dir. Thetest-commanddefault is empty and selectsnpm run test:phpin wp-env mode,vendor/bin/phpunitstandalone.ci-test-js.yml— reusable Jest unit-test workflow with optional cross-run cache (enable-cache), monorepo support (working-dir), and a consumer-overridable command (test-command). Replaces the copy-pastedunit-tests-jsjob from the legacyTest and Measureworkflow.ci-test-a11y.yml— reusable pa11y-ci accessibility test workflow. Order:npm ci→ build →wp-env start→ pa11y →wp-env stop(withif: always()so cleanup runs on failure). Inputs:node-version,build-command,test-command,working-dir. Trigger-agnostic — consumers wire theRun a11ylabel gate at the caller level.ci-build.yml: reusable production-build workflow that covers the three rtCamp project shapes (Node-only, Composer-only, both). Each stack is gated on its own toggle:install-node-deps(defaulttrue) andinstall-composer-deps(defaultfalse). Defaultcomposer-flagsuse--no-dev(production build) and keep--no-scriptsfor supply-chain hygiene. Thebuild-commanddefault is empty and selectsnpm run build:prodwhen Node deps install,composer run-script buildotherwise. Optionalupload-artifact(defaultfalse) hands the build output to a downstream CD job; usesif-no-files-found: errorso a misconfigured artifact path fails loudly at the upload step. Inputs:install-node-deps,node-version,install-composer-deps,php-version,composer-flags,build-command,upload-artifact,artifact-name,artifact-path,retention-days,working-dir.ci-build-artifact-gate.yml: reusable workflow that fails a PR if it commits files under configured path prefixes (default:assets/build/). Pair withci-build.ymlso the CI-produced bundle is the only source of truth for build output. Comparesorigin/<base>..HEADwith--diff-filter=ACMR, so additions/modifications/renames trip the gate and deletions are ignored (removing a previously-committed artifact is welcome). Runs only onpull_requestevents; on other triggers it logs a notice and exits 0 so the workflow stays inert outside PRs. Emptygated-pathsinput skips gracefully. Path prefixes are anchored as literal strings, and regex metacharacters in path names (. + * ( ) [ ] { } | ^ $ ? \) are escaped automatically. Inputs:gated-paths,working-dir.version-monitor.yml— monthly scheduled workflow that detects version bumps and opens a draft PR per repo on aversion-monitor/YYYY-MMbranch. Re-runs in the same month update the existing PR. Node.js version is set via thenode-versioninput (default22). Major bumps are surfaced for manual review; a month of only major bumps, or a detector that could not be checked, fails the run rather than passing silently.ci-test-build-artifact.yml: reusable workflow that boots the packaged build artifact in a real WordPress to prove it installs, not just that the build compiled. Downloads the named artifact (viaactions/download-artifactpinned tod3f86a1, v4.3.0), writes a.wp-env.override.jsonto pinphpVersion/coreand mount only the downloaded artifact (clearingplugins/themesfrom the consumer config so the dev source tree never collides with the packaged output), boots@wordpress/envwithnpx --yes @wordpress/env start --update, activates the plugin or theme (selected frominstall-path), then runswp doctor check --all --format=jsonand fails only onerror-severity checks (warnings allowed). The artifact must be a complete installable plugin/theme directory, so pointci-build.yml'sartifact-pathat a packaged dist directory rather than barebuild/. An optionalsmoke-scriptruns inside the container after activation, andwp-env stopruns in anif: always()step (with its own timeout) so no container leaks into the next job. Production-oriented doctor checks that a dev wp-env always trips (e.g.constant-wp-debug-falsy, since wp-env enablesWP_DEBUG) are excluded from the gate via thedoctor-ignore-checksinput. Inputs:artifact-name,slug,node-version,php-version,wp-version,install-path,smoke-script,doctor-ignore-checks.cd-github-release.yml: reusable workflow that publishes a GitHub Release on av*.*.*tag push. Downloads the named build artifact, extracts the release body from the matchingCHANGELOG.mdsection (tolerant heading match —## v1.2.3or## [1.2.3]with an optional trailing date;## Unreleasedis never matched and a missing/empty section fails the workflow rather than publishing empty notes), then creates the Release via theghCLI (no third-party action) with--verify-tagso a non-existent tag errors instead of silently creating one. Attaches the artifact files and honoursdraft/prerelease. Requires the caller to grantcontents: write; the artifact must be produced in the same run. Inputs:tag,artifact-name,changelog-path,draft,prerelease.cd-s3.yml: reusable workflow that distributes a private/customer plugin to an S3 bucket on a release tag. Downloads the named build artifact and uploads its single.zipto a versioned key (s3://<bucket>/<prefix><tag>.zip) plus an always-overwritten<prefix>latest.zip(fails if the artifact contains zero or more than one.zip, so a misconfiguredartifact-pathcan never deploy the wrong bundle), then optionally invalidates/<prefix>*on a CloudFront distribution whencloudfront-distribution-idis set. Uses the officialaws-actions/configure-aws-credentials(SHA-pinned to v6.1.3) with static access keys and the preinstalled AWS CLI — no third-party action, no checkout. User inputs pass through stepenv:(injection-safe). Inputs:tag,artifact-name,bucket,prefix,cloudfront-distribution-id,region. Required secrets:AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY(needs3:PutObjecton the prefix, pluscloudfront:CreateInvalidationwhen invalidating).cd-wp-org.yml: reusable workflow that deploys a plugin to the WordPress.org directory over SVN on a release tag, wrapping10up/action-wordpress-plugin-deploy(SHA-pinned to v2.3.0). Downloads the named build artifact, fails fast unlessreadme.txt'sStable tagmatches the tag (leadingvstripped), then deploys the artifact totrunk/+tags/<version>/via the action'sBUILD_DIRand syncsassets-path(default.wordpress-org, read from the checked-out repo) to SVN/assets/. The action is configured through environment variables (SLUG,VERSION,ASSETS_DIR,BUILD_DIR,SVN_USERNAME,SVN_PASSWORD);dry-runruns everything except the SVN commit; deploying a version that already exists fails (WP.org tags are immutable). Inputs:slug,tag,artifact-name,assets-path,dry-run. Required secrets:WP_ORG_USERNAME,WP_ORG_PASSWORD.ci-detect-changes.yml: reusable pre-run workflow that buckets a PR/push's changed files and exposes per-bucket change counts (total-count,ignored-count,css-count,js-count,php-count,gha-count) and file lists (css-files,js-files,php-files,gha-files) as workflow outputs. Wraps thewp-tooling detect-changesCLI; base-ref resolution and bucket rules live there. The*-filesoutputs feed straight into the lint workflows'changed-filesinput so the orchestrator can lint only changed files. Aworking-dirinput restricts the css/js/php counts and file lists to one directory and re-roots those lists against it — a path scope, not a working directory, since the diff is always taken from the repo root while the linters run inside that directory. Atworking-dir: "."it is a pure pass-through, counts included, so single-project consumers keep the CLI's own numbers.total-count/ignored-countandgha-count/gha-filesstay repo-wide: the CLI buckets only CSS/JS/PHP/GHA, and workflow files only exist at.github/workflows/in the repo root. A::warning::fires when the repository changed but nothing is in scope for the unit, so a green run that verified nothing is at least visible. Runs unconditionally — count-based gating is the caller's job. Inputs:node-version,ignore-paths,base-ref,working-dir,wp-tooling-ref.ci-lint-css.yml: reusable Stylelint workflow. Runslint-commandover the whole project, or only the files inchanged-fileswhen that input is non-empty (selective linting). Inputs:node-version,lint-command,changed-files,working-dir.ci-lint-js.yml: reusable ESLint workflow with optionalpackage.jsonvalidation (validate-package-json, defaulttrue).changed-filesscopes ESLint to just the listed files; empty lints the whole project. Inputs:node-version,lint-command,changed-files,validate-package-json,working-dir.ci-lint-php.yml: reusable PHPCS workflow withcs2prannotations and an optional PHPStan pass (enable-phpstan, defaultfalse).changed-filesscopes PHPCS to the listed files; PHPStan always runs whole-project because it analyses cross-file dependencies. Defaultcomposer-flagskeep--no-scriptsfor supply-chain hygiene. Inputs:php-version,enable-phpstan,phpcs-command,changed-files,phpstan-command,phpstan-level,composer-flags,validate-composer,working-dir.wp-cd.yml: CD orchestrator that fans out tocd-github-release.yml,cd-wp-org.ymlandcd-s3.ymlfrom one comma-separateddeploy-targetinput (github,wporg,s3). Avalidate-inputsjob fails the run before any deploy starts on an emptytagorartifact-name, an empty or whitespace-containing target list, an empty entry, an unknown target, or a listed target missing its required inputs or secrets (wporgneedsslug+WP_ORG_USERNAME/WP_ORG_PASSWORD,s3needsbucket+AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; secrets are presence-checked as booleans, so no secret material reaches the environment). Targets match comma-bounded, likewp-ci.yml'sskip, and run in parallel. Every leaf input passes through under its own name and default;slugalso sets the Release zip's root directory, andworking-dirreaches both leaves that read the source tree. There is nopermissionsblock, so the calling job must grantcontents: writeon every call — GitHub checks nested jobs' permissions against the caller's grant before it evaluatesif:, even whengithubis not listed. All four secrets are optional; map only the ones the listed targets need. Inputs:tag,artifact-name,deploy-target,slug,working-dir,changelog-path,draft,prerelease,assets-path,dry-run,bucket,prefix,cloudfront-distribution-id,region.wp-ci.yml: CI orchestrator that routes to the individual CI workflows from oneproject-typeinput (plugin|theme|package).detect-changesruns first as a never-skippable backbone; every other job is gated on its per-bucket change counts and fed the matching*-fileslist, so a job runs only when its file type changed and its preset includes it.skipdrops jobs by id with comma-exact matching (skip: builddoes not also dropbuild-artifact).test-phpfans out overphp-versions×wp-versionsminustest-php-excludecells;use-wp-env(defaulttrue) boots@wordpress/envper cell, pinningWP_ENV_PHP_VERSION/WP_ENV_CORE, whilefalseruns standalone PHPUnit for a pure Composer library and leaves thewpdimension inert.enable-phpstan(defaultfalse) optslint-phpinto a PHPStan pass. Avalidate-inputsjob fails the run fast on an invalidproject-typeor a non-array matrix input, resolves the job preset from a single table, and derives a per-unitartifact-name(two units uploading the same name in one run is a409 Conflict) and abuild-artifact-slugdefaulting tobasename(working-dir). Thebuild-artifactinstall test is opt-in viabuild-artifact-path;artifact-gateis opt-in viagated-paths, since failing a PR for committed build output is only correct where CI produces it. Aworking-dirinput targets one unit in a monorepo and is forwarded to every child job, soproject-typedescribes the unit atworking-dirrather than the repository — a repo with a theme and two plugins calls this workflow three times, or matrixes over the units in one caller job. Concurrency includesproject-typeandworking-dirso sibling calls do not cancel each other. Inputs:project-type,working-dir,skip,enable-phpstan,run-a11y,use-wp-env,php-versions,wp-versions,test-php-exclude,build-artifact-path,build-artifact-slug,ignore-paths,base-ref,gated-paths,wp-tooling-ref. Code coverage reporting is not yet included (v1.x follow-up).
Fixed
ci-lint-css.ymlno longer passespackage.jsonandpackage-lock.jsonto Stylelint.ci-detect-changeslists them incss-filesso that a dependency change re-runs the job, and Stylelint failed on them withCssSyntaxError. Whenchanged-filesholds anything other than stylesheets, or is empty, the job now runslint-commandover the whole project.ci-lint-php.ymlno longer passes deleted files or non-PHP files to PHPCS.ci-detect-changeslistscomposer.json,composer.lockand the PHPStan config inphp-files, and a change that deletes PHP files lists them too, so PHPCS failed withdoes not existorStart tag expected. Changed PHP files that still exist are linted file by file; if the list holds anything else, PHPCS lints the whole project.