Reusable GitHub Actions workflows for rtCamp WordPress projects. Pure YAML — nothing to install.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
ci:
uses: rtCamp/wp-shared-workflows/.github/workflows/wp-ci.yml@v1
with:
project-type: plugin # plugin | theme | packageThat is a complete CI setup: lint CSS/JS/PHP, Jest, PHPUnit, and build — each job gated on what actually changed, so a docs-only PR runs almost nothing.
Every job runs on the runner the caller picks with the runs-on input, as JSON. It defaults to
GitHub-hosted "ubuntu-latest". A reusable workflow's jobs run in the calling repository, so a public
repository cannot use rtCamp's self-hosted runners, while rtCamp's private repositories must (org
runner policy) and pass:
with:
runs-on: '["self-hosted"]'wp-ci.yml and wp-cd.yml forward it to every workflow they call.
Node comes from .nvmrc in working-dir, then the repository root, falling back to 22; set
node-version to override it. wp-ci.yml also forwards php-version, the Composer settings and a
*-command input per job, each falling back to that job's default when left empty.
Each workflow declares its inputs, secrets and outputs in its own workflow_call block, and GitHub
renders those descriptions when you wire up a caller. Every example is a complete consumer workflow
you can copy as-is.
| Workflow | What it does | Example |
|---|---|---|
wp-ci.yml |
CI orchestrator. One call per unit; routes to the right jobs for project-type |
example |
ci-detect-changes.yml |
Buckets changed files, exposes per-type counts and file lists | example |
ci-lint-css.yml |
Stylelint | example |
ci-lint-js.yml |
ESLint + optional package.json validation |
example |
ci-lint-php.yml |
PHPCS with cs2pr annotations + optional PHPStan |
example |
ci-test-js.yml |
Jest | example |
ci-test-php.yml |
PHPUnit, via @wordpress/env or standalone |
example |
ci-test-a11y.yml |
pa11y-ci WCAG2AA | example |
ci-build.yml |
Production build + optional artifact upload | example |
ci-build-artifact-gate.yml |
Fails a PR that commits build output | example |
ci-test-build-artifact.yml |
Boots the packaged artifact in real WordPress | example |
Each is opt-in — call the ones you need from your own release trigger, or fan out to several from
one wp-cd.yml call.
| Workflow | What it does | Example |
|---|---|---|
wp-cd.yml |
CD orchestrator. One call fans out to github, wporg and s3 from deploy-target |
example |
cd-github-release.yml |
GitHub Release from a tag + artifact, notes from CHANGELOG.md; attaches to the release instead when one exists |
example |
cd-wp-org.yml |
WordPress.org SVN deploy | example |
cd-s3.yml |
S3 upload + optional CloudFront invalidation | example |
cd-built-branch.yml |
Force-pushes source + generated output to a deploy branch | example |
release-please creates the tag and the GitHub Release with GITHUB_TOKEN, which never starts an
on: push: tags workflow. Build and attach in the same workflow instead, gated on its output;
cd-github-release.yml sees the existing release and only uploads the zip:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: read
jobs:
release-please:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
outputs:
release-created: ${{ steps.release.outputs.release_created }}
tag: ${{ steps.release.outputs.tag_name }}
steps:
- id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
build:
needs: release-please
if: needs.release-please.outputs.release-created == 'true'
uses: rtCamp/wp-shared-workflows/.github/workflows/ci-build.yml@v1
with:
build-command: npm run build:prod && npm run package
upload-artifact: true
artifact-name: acme-blocks
artifact-path: dist/acme-blocks
release:
needs: [release-please, build]
permissions:
contents: write
uses: rtCamp/wp-shared-workflows/.github/workflows/cd-github-release.yml@v1
with:
tag: ${{ needs.release-please.outputs.tag }}
artifact-name: acme-blocksrelease-please-action reads release-please-config.json and .release-please-manifest.json from
the repository root. For a manifest package at a path, read
steps.release.outputs['plugins/acme-blocks--release_created'] and …--tag_name instead.
With immutable releases enabled, a published release takes no new assets. Set "draft": true and
"force-tag-creation": true in release-please-config.json, and publish once the zip is attached:
publish:
needs: [release-please, release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish the draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release-please.outputs.tag }}
run: gh release edit "$TAG" --draft=false| Workflow | What it does | Example |
|---|---|---|
version-monitor.yml |
Monthly version-bump check that opens a draft PR | example |
Pin a version tag. @v1 and @v1.2 are aliases that move forward to compatible releases; an exact
@v1.2.3 never changes.
| Pin | Resolves to |
|---|---|
@v1 |
Latest v1.x.y — recommended |
@v1.2 |
Latest patch of the v1.2 line |
@v1.2.3 |
One exact release |
Releases are cut by release-please from the Conventional Commits merged to main.
Within a major version, inputs are only ever added, and always with a default. Removing or renaming an input, or making one required, ships as a new major.
Conventions live in AGENTS.md; the PR process is in CONTRIBUTING.md.
Every example is verified in CI by bin/check-workflows.sh: the with:
and secrets: keys must exist on the workflow being called, every required input must be supplied,
and the ref must be @v1. An example cannot silently drift out of date.
GPL-2.0-or-later. See LICENSE.
