Skip to content

v1.0.0

Choose a tag to compare

@aryanjasala aryanjasala released this 01 Oct 21:32
9b8648f

First tagged release. Consumers pin @v1.

Changed

  • Every reusable workflow takes its runner from a new runs-on input (JSON: a label or a list of labels), defaulting to GitHub-hosted "ubuntu-latest", and wp-ci.yml and wp-cd.yml forward 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.yml runs on ubuntu-latest, and bin/check-workflows.sh now 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 convention AGENTS.md already documented but sixteen steps across eight workflows broke. Value inputs (composer-flags, phpstan-level) now travel through step env: 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, and cd-built-branch.yml's per-target equivalents) also travel through env: and are run as bash -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 pipefail is not optional — a bare bash -c does not inherit set -e, so a failing command would exit 0 and pass a job green. bin/check-workflows.sh now fails on any ${{ inputs. }}/${{ matrix. }} inside a run: body, so the convention is verified rather than merely documented.

  • cd-github-release.yml packages the release asset instead of attaching the artifact's top-level entries directly. gh release create only accepts files, but download-artifact unpacks a directory artifact, so any plugin with a subdirectory — including the shape examples/cd-github-release.yml produces — failed at the upload. A directory artifact is now re-rooted under <slug>/ and zipped to a single <slug>.zip that installs through wp-admin; an artifact that is already a single archive is attached untouched, so a pre-zipped artifact-path keeps working. The two cases are told apart by archive extension rather than file count, so a single-file plugin still gets packaged. Adds an optional slug input (defaults to artifact-name).

  • ci-test-js.yml's enable-cache now caches something. Jest has no JEST_CACHE_DIRECTORY environment variable, so the previous wiring cached an always-empty directory, and the fixed jest-<os> key was written once and never updated again. The cache directory is now passed as a real --cacheDirectory argument and the key includes the working-dir-scoped lockfile hash with a restore-keys fallback. A custom test-command must forward its arguments for the flag to land; the default does.

  • ci-self-check.yml runs yamllint over the whole repository rather than just .github/workflows/ and examples/, matching the yamllint . that AGENTS.md tells contributors to run — previously .yamllint.yml and .github/actionlint.yaml could fail locally while CI stayed green.

  • cd-built-branch.yml's published and published-sha output descriptions now cover every no-push outcome. A run superseded by a newer one also reports published=false with an empty SHA, so false never 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.md and CONTRIBUTING.md describe bin/ as Bash, not POSIX shell — bin/check-workflows.sh uses BASH_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 of README.md into examples/, one runnable file per workflow, verified in CI by bin/check-workflows.sh. README.md is now a workflow index plus a versioning guide, and no longer duplicates the input tables — each workflow's workflow_call block is the single source of truth, and GitHub renders those descriptions when a caller is configured. Repo conventions move to a tool-agnostic AGENTS.md, which CLAUDE.md imports. Adds LICENSE (GPL-2.0-or-later, previously declared but absent) and CONTRIBUTING.md.

  • version-monitor.yml now pins actions/checkout and actions/setup-node to the same SHAs as the other fifteen workflows, and its job carries an explicit name:.

  • The wp-tooling-token secret is removed from ci-detect-changes.yml, version-monitor.yml and wp-ci.yml. rtCamp/wp-tooling is public, so the CLI now installs straight from its npm/wp-tooling branch 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 with Invalid input, wp-tooling-token is not defined in the referenced workflow. A new wp-tooling-ref input (forwarded through wp-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-token secret is removed from ci-lint-php.yml, ci-test-php.yml and wp-ci.yml. rtCamp/wp-framework is public, so no known private Composer dependency is left to authenticate. It was added in 6274f32 and 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 with Invalid input, packages-token is not defined in the referenced workflow. setup-php still receives github-token: ${{ github.token }} for the API rate limit, which never depended on the secret, and ci-test-php.yml drops the wp-env Composer-auth step that existed only to forward it into the container.

  • Monorepo fixes across the CI leaves. All six setup-node steps that use cache: npm now pass cache-dependency-path, and ci-lint-php.yml's Composer cache key is scoped to <working-dir>/composer.lock. ci-build-artifact-gate.yml normalises and regex-escapes working-dir before 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.yml gains working-dir.

  • cd-github-release.yml and cd-wp-org.yml gain working-dir as 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's CHANGELOG.md still parses, and a missing .wordpress-org only emits a notice. Both now reject absolute and .. paths. cd-s3.yml is unchanged.

  • cd-github-release.yml, cd-wp-org.yml and cd-s3.yml fail fast in a first validation step when a required input arrives empty: tag and artifact-name everywhere, plus slug for wporg and bucket for s3. required: true does not reject an empty string, and an empty artifact-name makes download-artifact fetch every artifact in the run; an empty slug would fall back to the deploy action's repo-name default and could deploy to the wrong wordpress.org plugin, and an empty tag would upload to <prefix>.zip. cd-wp-org.yml and cd-s3.yml also 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.sh applies 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, so cd-s3.yml installs a pinned, checksum-verified one when the runner has none.

  • ci-detect-changes.yml, and so wp-ci.yml, now fetch an explicit base-ref before diffing. The CLI diffs an explicit base as-is, and the depth-2 checkout never holds a branch such as main, 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.yml pins wp-cli/doctor-command to 2.3.1. @stable resolved 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.md and CONTRIBUTING.md now base task branches and pull requests on main, add pipx to the install line because pipx run yamllint needs 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 (Composer vendor/, compiled assets/build/) to a dedicated branch, for platforms that deploy from a git branch rather than an artifact. Three jobs: discover resolves the targets JSON into a build matrix, build builds each target in parallel, and publish force-pushes a single commit whose parent is the source commit — so git diff <source>..<target> is exactly the generated files, and rollback is a dispatch with source-ref plus allow-rewind: true. Every run republishes every target, so the branch is always a complete tree; a target whose dir is 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 explicit source-ref pointing backwards errors unless allow-rewind: true. Only publish holds contents: write, so the job that executes third-party dependency code never has a writable token; the caller must grant permissions: 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 default GITHUB_TOKEN never does. Outputs: published, published-sha, source-sha, targets.
  • ci-test-php.yml: reusable PHPUnit workflow with a use-wp-env toggle (default true) for WordPress integration tests via @wordpress/env, or standalone PHPUnit for Composer-only packages and libraries. In wp-env mode, php-version is pinned into the container via WP_ENV_PHP_VERSION and wp-version (when non-empty) sets WP_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 empty wp-version uses the consumer's .wp-env.json. Inputs: php-version, node-version, wp-version, use-wp-env, test-command, composer-flags, working-dir. The test-command default is empty and selects npm run test:php in wp-env mode, vendor/bin/phpunit standalone.
  • 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-pasted unit-tests-js job from the legacy Test and Measure workflow.
  • ci-test-a11y.yml — reusable pa11y-ci accessibility test workflow. Order: npm ci → build → wp-env start → pa11y → wp-env stop (with if: always() so cleanup runs on failure). Inputs: node-version, build-command, test-command, working-dir. Trigger-agnostic — consumers wire the Run a11y label 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 (default true) and install-composer-deps (default false). Default composer-flags use --no-dev (production build) and keep --no-scripts for supply-chain hygiene. The build-command default is empty and selects npm run build:prod when Node deps install, composer run-script build otherwise. Optional upload-artifact (default false) hands the build output to a downstream CD job; uses if-no-files-found: error so 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 with ci-build.yml so the CI-produced bundle is the only source of truth for build output. Compares origin/<base>..HEAD with --diff-filter=ACMR, so additions/modifications/renames trip the gate and deletions are ignored (removing a previously-committed artifact is welcome). Runs only on pull_request events; on other triggers it logs a notice and exits 0 so the workflow stays inert outside PRs. Empty gated-paths input 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 a version-monitor/YYYY-MM branch. Re-runs in the same month update the existing PR. Node.js version is set via the node-version input (default 22). 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 (via actions/download-artifact pinned to d3f86a1, v4.3.0), writes a .wp-env.override.json to pin phpVersion/core and mount only the downloaded artifact (clearing plugins/themes from the consumer config so the dev source tree never collides with the packaged output), boots @wordpress/env with npx --yes @wordpress/env start --update, activates the plugin or theme (selected from install-path), then runs wp doctor check --all --format=json and fails only on error-severity checks (warnings allowed). The artifact must be a complete installable plugin/theme directory, so point ci-build.yml's artifact-path at a packaged dist directory rather than bare build/. An optional smoke-script runs inside the container after activation, and wp-env stop runs in an if: 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 enables WP_DEBUG) are excluded from the gate via the doctor-ignore-checks input. 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 a v*.*.* tag push. Downloads the named build artifact, extracts the release body from the matching CHANGELOG.md section (tolerant heading match — ## v1.2.3 or ## [1.2.3] with an optional trailing date; ## Unreleased is never matched and a missing/empty section fails the workflow rather than publishing empty notes), then creates the Release via the gh CLI (no third-party action) with --verify-tag so a non-existent tag errors instead of silently creating one. Attaches the artifact files and honours draft / prerelease. Requires the caller to grant contents: 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 .zip to 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 misconfigured artifact-path can never deploy the wrong bundle), then optionally invalidates /<prefix>* on a CloudFront distribution when cloudfront-distribution-id is set. Uses the official aws-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 step env: (injection-safe). Inputs: tag, artifact-name, bucket, prefix, cloudfront-distribution-id, region. Required secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (need s3:PutObject on the prefix, plus cloudfront:CreateInvalidation when invalidating).
  • cd-wp-org.yml: reusable workflow that deploys a plugin to the WordPress.org directory over SVN on a release tag, wrapping 10up/action-wordpress-plugin-deploy (SHA-pinned to v2.3.0). Downloads the named build artifact, fails fast unless readme.txt's Stable tag matches the tag (leading v stripped), then deploys the artifact to trunk/ + tags/<version>/ via the action's BUILD_DIR and syncs assets-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-run runs 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 the wp-tooling detect-changes CLI; base-ref resolution and bucket rules live there. The *-files outputs feed straight into the lint workflows' changed-files input so the orchestrator can lint only changed files. A working-dir input 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. At working-dir: "." it is a pure pass-through, counts included, so single-project consumers keep the CLI's own numbers. total-count/ignored-count and gha-count/gha-files stay 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. Runs lint-command over the whole project, or only the files in changed-files when that input is non-empty (selective linting). Inputs: node-version, lint-command, changed-files, working-dir.
  • ci-lint-js.yml: reusable ESLint workflow with optional package.json validation (validate-package-json, default true). changed-files scopes 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 with cs2pr annotations and an optional PHPStan pass (enable-phpstan, default false). changed-files scopes PHPCS to the listed files; PHPStan always runs whole-project because it analyses cross-file dependencies. Default composer-flags keep --no-scripts for 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 to cd-github-release.yml, cd-wp-org.yml and cd-s3.yml from one comma-separated deploy-target input (github, wporg, s3). A validate-inputs job fails the run before any deploy starts on an empty tag or artifact-name, an empty or whitespace-containing target list, an empty entry, an unknown target, or a listed target missing its required inputs or secrets (wporg needs slug + WP_ORG_USERNAME/WP_ORG_PASSWORD, s3 needs bucket + 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, like wp-ci.yml's skip, and run in parallel. Every leaf input passes through under its own name and default; slug also sets the Release zip's root directory, and working-dir reaches both leaves that read the source tree. There is no permissions block, so the calling job must grant contents: write on every call — GitHub checks nested jobs' permissions against the caller's grant before it evaluates if:, even when github is 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 one project-type input (plugin | theme | package). detect-changes runs first as a never-skippable backbone; every other job is gated on its per-bucket change counts and fed the matching *-files list, so a job runs only when its file type changed and its preset includes it. skip drops jobs by id with comma-exact matching (skip: build does not also drop build-artifact). test-php fans out over php-versions × wp-versions minus test-php-exclude cells; use-wp-env (default true) boots @wordpress/env per cell, pinning WP_ENV_PHP_VERSION/WP_ENV_CORE, while false runs standalone PHPUnit for a pure Composer library and leaves the wp dimension inert. enable-phpstan (default false) opts lint-php into a PHPStan pass. A validate-inputs job fails the run fast on an invalid project-type or a non-array matrix input, resolves the job preset from a single table, and derives a per-unit artifact-name (two units uploading the same name in one run is a 409 Conflict) and a build-artifact-slug defaulting to basename(working-dir). The build-artifact install test is opt-in via build-artifact-path; artifact-gate is opt-in via gated-paths, since failing a PR for committed build output is only correct where CI produces it. A working-dir input targets one unit in a monorepo and is forwarded to every child job, so project-type describes the unit at working-dir rather 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 includes project-type and working-dir so 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.yml no longer passes package.json and package-lock.json to Stylelint. ci-detect-changes lists them in css-files so that a dependency change re-runs the job, and Stylelint failed on them with CssSyntaxError. When changed-files holds anything other than stylesheets, or is empty, the job now runs lint-command over the whole project.
  • ci-lint-php.yml no longer passes deleted files or non-PHP files to PHPCS. ci-detect-changes lists composer.json, composer.lock and the PHPStan config in php-files, and a change that deletes PHP files lists them too, so PHPCS failed with does not exist or Start tag expected. Changed PHP files that still exist are linted file by file; if the list holds anything else, PHPCS lints the whole project.