From 7f412522519877491175c418ca88357e94c19eb9 Mon Sep 17 00:00:00 2001 From: Aryan Jasala Date: Fri, 6 Nov 2026 05:12:42 +0530 Subject: [PATCH 1/3] fix(scaffolds): call wp-shared-workflows at @v1 and let .nvmrc pick Node The lint-css, lint-js and lint-php scaffolds defaulted wsw_ref to the deleted v1.0.0/task/ci-lint-workflows branch, so a workflow scaffolded with the defaults failed at startup. test-measure defaulted to the stale release/v1.0.0 branch and cd-wporg hard-coded @v1.0.0. All now call @v1, and the lint templates drop the outdated EXPERIMENTAL header. node_version defaults to empty in the six scaffolds that had 22, and the templates leave node-version out when it is empty, so the shared workflow reads the project's .nvmrc (wp-shared-workflows v1.1.0). An explicit --node_version still pins a version. test-measure's default WordPress versions gain 7.1. --- node-packages/wp-tooling/CHANGELOG.md | 6 ++++++ node-packages/wp-tooling/scaffolds/ci/build/scaffold.json | 4 ++-- .../scaffolds/ci/build/templates/ci-build.yml.mustache | 4 ++-- .../scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache | 2 +- .../wp-tooling/scaffolds/ci/lint-css/scaffold.json | 8 ++++---- .../ci/lint-css/templates/ci-lint-css.yml.mustache | 8 ++------ .../wp-tooling/scaffolds/ci/lint-js/scaffold.json | 8 ++++---- .../ci/lint-js/templates/ci-lint-js.yml.mustache | 8 ++------ .../wp-tooling/scaffolds/ci/lint-php/scaffold.json | 4 ++-- .../ci/lint-php/templates/ci-lint-php.yml.mustache | 4 ---- .../wp-tooling/scaffolds/ci/test-a11y/scaffold.json | 4 ++-- .../ci/test-a11y/templates/ci-test-a11y.yml.mustache | 4 ++-- .../wp-tooling/scaffolds/ci/test-js/scaffold.json | 4 ++-- .../ci/test-js/templates/ci-test-js.yml.mustache | 4 ++-- .../wp-tooling/scaffolds/ci/test-measure/scaffold.json | 6 +++--- .../wp-tooling/scaffolds/ci/test-php/scaffold.json | 4 ++-- .../ci/test-php/templates/ci-test-php.yml.mustache | 4 ++-- 17 files changed, 40 insertions(+), 46 deletions(-) diff --git a/node-packages/wp-tooling/CHANGELOG.md b/node-packages/wp-tooling/CHANGELOG.md index 6448b6d..8f658e7 100644 --- a/node-packages/wp-tooling/CHANGELOG.md +++ b/node-packages/wp-tooling/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- The CI scaffolds call rtCamp/wp-shared-workflows at `@v1`. `ci/lint-css`, `ci/lint-js` and `ci/lint-php` defaulted to the `v1.0.0/task/ci-lint-workflows` branch, which no longer exists, so a workflow scaffolded with the defaults failed before any job ran. `ci/test-measure` defaulted to the stale `release/v1.0.0` branch, and `ci/cd-wporg` hard-coded `@v1.0.0`. The lint templates also drop their outdated "EXPERIMENTAL" header. +- `node_version` in the `ci/build`, `ci/lint-css`, `ci/lint-js`, `ci/test-a11y`, `ci/test-js` and `ci/test-php` scaffolds defaults to empty, which leaves `node-version` out of the generated workflow, so the shared workflow uses the project's `.nvmrc`. The old default of `22` overrode `.nvmrc`. Passing `--node_version` still pins a version. +- `ci/test-measure`'s default WordPress versions include 7.1. + ### Changed - `detect-changes` counts `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts` and `.tsx` files in the `js` bucket, not just `.js` and Jest snapshots, so ES-module and TypeScript changes trigger the jobs gated on `js-count`. diff --git a/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json index 8812879..db004e7 100644 --- a/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json @@ -12,8 +12,8 @@ }, { "key": "node_version", - "description": "Node.js version for the build.", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" }, { "key": "php_version", diff --git a/node-packages/wp-tooling/scaffolds/ci/build/templates/ci-build.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/build/templates/ci-build.yml.mustache index 0411f1a..eb76e83 100644 --- a/node-packages/wp-tooling/scaffolds/ci/build/templates/ci-build.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/build/templates/ci-build.yml.mustache @@ -20,7 +20,7 @@ jobs: uses: rtCamp/wp-shared-workflows/.github/workflows/ci-build.yml@{{wsw_ref}} with: install-node-deps: true - install-composer-deps: {{install_composer_deps}} - node-version: "{{node_version}}" + install-composer-deps: {{install_composer_deps}}{{#node_version}} + node-version: "{{node_version}}"{{/node_version}} php-version: "{{php_version}}" upload-artifact: {{upload_artifact}} diff --git a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache index bcf99ad..a457a34 100644 --- a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache @@ -6,7 +6,7 @@ on: jobs: deploy: - uses: rtCamp/wp-shared-workflows/.github/workflows/cd-wp-org.yml@v1.0.0 + uses: rtCamp/wp-shared-workflows/.github/workflows/cd-wp-org.yml@v1 with: plugin-slug: {{plugin_slug}} secrets: diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json index 4d2ebca..9ee189c 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json @@ -7,13 +7,13 @@ "inputs": [ { "key": "wsw_ref", - "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Workflow is in PR #2 at the time of writing; pin to `v1.0.0/task/ci-lint-workflows` or a commit SHA until the PR is merged.", - "default": "v1.0.0/task/ci-lint-workflows" + "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Pin a major tag such as `v1`, never a branch: branches move or get deleted.", + "default": "v1" }, { "key": "node_version", - "description": "Node.js version.", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" } ], "files": [ diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-css/templates/ci-lint-css.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/lint-css/templates/ci-lint-css.yml.mustache index 4fe320f..36193f0 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-css/templates/ci-lint-css.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/lint-css/templates/ci-lint-css.yml.mustache @@ -3,10 +3,6 @@ name: CI / Lint (CSS) # Scaffolded by @rtcamp/wp-tooling. # Calls the reusable ci-lint-css.yml from rtCamp/wp-shared-workflows. # -# EXPERIMENTAL: this reusable workflow is currently in rtCamp/wp-shared-workflows -# PR #2 and has not yet shipped on the `v1` tag. The `uses:` line below pins to -# the PR branch; switch to `@v1` (or a SHA) once the PR is merged. -# # Source: https://github.com/rtCamp/wp-shared-workflows/blob/{{wsw_ref}}/.github/workflows/ci-lint-css.yml on: @@ -22,6 +18,6 @@ permissions: jobs: lint-css: - uses: rtCamp/wp-shared-workflows/.github/workflows/ci-lint-css.yml@{{wsw_ref}} + uses: rtCamp/wp-shared-workflows/.github/workflows/ci-lint-css.yml@{{wsw_ref}}{{#node_version}} with: - node-version: "{{node_version}}" + node-version: "{{node_version}}"{{/node_version}} diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json index eea78b9..33c3b14 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json @@ -7,13 +7,13 @@ "inputs": [ { "key": "wsw_ref", - "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Workflow is in PR #2 at the time of writing; pin to `v1.0.0/task/ci-lint-workflows` or a commit SHA until the PR is merged.", - "default": "v1.0.0/task/ci-lint-workflows" + "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Pin a major tag such as `v1`, never a branch: branches move or get deleted.", + "default": "v1" }, { "key": "node_version", - "description": "Node.js version.", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" }, { "key": "validate_package_json", diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-js/templates/ci-lint-js.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/lint-js/templates/ci-lint-js.yml.mustache index ab697a9..6e59f82 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-js/templates/ci-lint-js.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/lint-js/templates/ci-lint-js.yml.mustache @@ -3,10 +3,6 @@ name: CI / Lint (JS) # Scaffolded by @rtcamp/wp-tooling. # Calls the reusable ci-lint-js.yml from rtCamp/wp-shared-workflows. # -# EXPERIMENTAL: this reusable workflow is currently in rtCamp/wp-shared-workflows -# PR #2 and has not yet shipped on the `v1` tag. The `uses:` line below pins to -# the PR branch; switch to `@v1` (or a SHA) once the PR is merged. -# # Source: https://github.com/rtCamp/wp-shared-workflows/blob/{{wsw_ref}}/.github/workflows/ci-lint-js.yml on: @@ -23,6 +19,6 @@ permissions: jobs: lint-js: uses: rtCamp/wp-shared-workflows/.github/workflows/ci-lint-js.yml@{{wsw_ref}} - with: - node-version: "{{node_version}}" + with:{{#node_version}} + node-version: "{{node_version}}"{{/node_version}} validate-package-json: {{validate_package_json}} diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json index a1eee0e..6d24b16 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json @@ -7,8 +7,8 @@ "inputs": [ { "key": "wsw_ref", - "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Workflow is in PR #2 at the time of writing; pin to `v1.0.0/task/ci-lint-workflows` or a commit SHA until the PR is merged into v1.", - "default": "v1.0.0/task/ci-lint-workflows" + "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Pin a major tag such as `v1`, never a branch: branches move or get deleted.", + "default": "v1" }, { "key": "php_version", diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-php/templates/ci-lint-php.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/lint-php/templates/ci-lint-php.yml.mustache index 1498c8e..b6b5fc1 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-php/templates/ci-lint-php.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/lint-php/templates/ci-lint-php.yml.mustache @@ -3,10 +3,6 @@ name: CI / Lint (PHP) # Scaffolded by @rtcamp/wp-tooling. # Calls the reusable ci-lint-php.yml from rtCamp/wp-shared-workflows. # -# EXPERIMENTAL: this reusable workflow is currently in rtCamp/wp-shared-workflows -# PR #2 and has not yet shipped on the `v1` tag. The `uses:` line below pins to -# the PR branch; switch to `@v1` (or a SHA) once the PR is merged. -# # Source: https://github.com/rtCamp/wp-shared-workflows/blob/{{wsw_ref}}/.github/workflows/ci-lint-php.yml on: diff --git a/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json index 9801979..84fb89e 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json @@ -12,8 +12,8 @@ }, { "key": "node_version", - "description": "Node.js version.", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" } ], "files": [ diff --git a/node-packages/wp-tooling/scaffolds/ci/test-a11y/templates/ci-test-a11y.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/test-a11y/templates/ci-test-a11y.yml.mustache index cfe72b0..f6274ca 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-a11y/templates/ci-test-a11y.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/test-a11y/templates/ci-test-a11y.yml.mustache @@ -19,6 +19,6 @@ permissions: jobs: test-a11y: - uses: rtCamp/wp-shared-workflows/.github/workflows/ci-test-a11y.yml@{{wsw_ref}} + uses: rtCamp/wp-shared-workflows/.github/workflows/ci-test-a11y.yml@{{wsw_ref}}{{#node_version}} with: - node-version: "{{node_version}}" + node-version: "{{node_version}}"{{/node_version}} diff --git a/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json index 3d6a672..3dc58be 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json @@ -12,8 +12,8 @@ }, { "key": "node_version", - "description": "Node.js version.", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" }, { "key": "enable_cache", diff --git a/node-packages/wp-tooling/scaffolds/ci/test-js/templates/ci-test-js.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/test-js/templates/ci-test-js.yml.mustache index 0fc424e..adf2401 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-js/templates/ci-test-js.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/test-js/templates/ci-test-js.yml.mustache @@ -18,6 +18,6 @@ permissions: jobs: test-js: uses: rtCamp/wp-shared-workflows/.github/workflows/ci-test-js.yml@{{wsw_ref}} - with: - node-version: "{{node_version}}" + with:{{#node_version}} + node-version: "{{node_version}}"{{/node_version}} enable-cache: {{enable_cache}} diff --git a/node-packages/wp-tooling/scaffolds/ci/test-measure/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-measure/scaffold.json index 5d68fa9..7d1a078 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-measure/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-measure/scaffold.json @@ -7,8 +7,8 @@ "inputs": [ { "key": "wsw_ref", - "description": "Git ref of rtCamp/wp-shared-workflows to pin (tag, branch, or SHA).", - "default": "release/v1.0.0" + "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Pin a major tag such as `v1`, never a branch: branches move or get deleted.", + "default": "v1" }, { "key": "project_type", @@ -28,7 +28,7 @@ { "key": "wp_versions", "description": "JSON array of WordPress core versions for the test-php matrix, crossed with php_versions.", - "default": "[\"6.7\", \"6.8\", \"6.9\", \"7.0\"]" + "default": "[\"6.7\", \"6.8\", \"6.9\", \"7.0\", \"7.1\"]" }, { "key": "test_php_exclude", diff --git a/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json index 48dc3f9..d1f5e2a 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json @@ -17,8 +17,8 @@ }, { "key": "node_version", - "description": "Node.js version (used only when use-wp-env is true).", - "default": "22" + "description": "Node.js version. Leave empty to use the project's .nvmrc, which the shared workflow reads.", + "default": "" }, { "key": "use_wp_env", diff --git a/node-packages/wp-tooling/scaffolds/ci/test-php/templates/ci-test-php.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/test-php/templates/ci-test-php.yml.mustache index 80c0d06..a8440b5 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-php/templates/ci-test-php.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/test-php/templates/ci-test-php.yml.mustache @@ -19,6 +19,6 @@ jobs: test-php: uses: rtCamp/wp-shared-workflows/.github/workflows/ci-test-php.yml@{{wsw_ref}} with: - php-version: "{{php_version}}" - node-version: "{{node_version}}" + php-version: "{{php_version}}"{{#node_version}} + node-version: "{{node_version}}"{{/node_version}} use-wp-env: {{use_wp_env}} From 6d7c074e921045985b6fa619c6fe676952f47c97 Mon Sep 17 00:00:00 2001 From: Aryan Jasala Date: Fri, 6 Nov 2026 05:53:54 +0530 Subject: [PATCH 2/3] fix(scaffolds): make ci/cd-wporg deploy for real and test every CI scaffold ci/cd-wporg passed inputs and secrets cd-wp-org.yml never had (plugin-slug, svn-username, svn-password) and deployed an artifact that nothing built, so it failed before any job ran. It now builds with ci-build.yml, packages with wp-tooling release:zip (which honours .distignore), unpacks with python3 (self-hosted runners have no unzip) and deploys with cd-wp-org.yml's slug, tag, artifact-name and WP_ORG_USERNAME/WP_ORG_PASSWORD. New inputs: wsw_ref, build_command, assets_path. The descriptions wp-tooling list prints drop the EXPERIMENTAL wording and the deleted-branch advice. A new test renders every ci/* scaffold, with and without node_version, and checks each reusable-workflow call against a snapshot of the wp-shared-workflows v1 workflow_call contracts: no empty with: or secrets: block, every call at @v1, no unknown input or secret, and every required one present. It fails on the old cd-wporg template. --- node-packages/wp-tooling/CHANGELOG.md | 4 +- .../scaffolds/ci/artifact-gate/scaffold.json | 2 +- .../scaffolds/ci/build/scaffold.json | 2 +- .../scaffolds/ci/cd-wporg/scaffold.json | 17 +- .../cd-wporg/templates/cd-wporg.yml.mustache | 32 +- .../scaffolds/ci/lint-css/scaffold.json | 2 +- .../scaffolds/ci/lint-js/scaffold.json | 2 +- .../scaffolds/ci/lint-php/scaffold.json | 2 +- .../scaffolds/ci/test-a11y/scaffold.json | 2 +- .../scaffolds/ci/test-js/scaffold.json | 2 +- .../scaffolds/ci/test-php/scaffold.json | 2 +- .../wp-shared-workflows-v1-contracts.json | 602 ++++++++++++++++++ .../tests/scaffolds/ci-workflows.test.js | 222 +++++++ 13 files changed, 879 insertions(+), 14 deletions(-) create mode 100644 node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json create mode 100644 node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js diff --git a/node-packages/wp-tooling/CHANGELOG.md b/node-packages/wp-tooling/CHANGELOG.md index 8f658e7..e392b2f 100644 --- a/node-packages/wp-tooling/CHANGELOG.md +++ b/node-packages/wp-tooling/CHANGELOG.md @@ -8,7 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed -- The CI scaffolds call rtCamp/wp-shared-workflows at `@v1`. `ci/lint-css`, `ci/lint-js` and `ci/lint-php` defaulted to the `v1.0.0/task/ci-lint-workflows` branch, which no longer exists, so a workflow scaffolded with the defaults failed before any job ran. `ci/test-measure` defaulted to the stale `release/v1.0.0` branch, and `ci/cd-wporg` hard-coded `@v1.0.0`. The lint templates also drop their outdated "EXPERIMENTAL" header. +- The CI scaffolds call rtCamp/wp-shared-workflows at `@v1`. `ci/lint-css`, `ci/lint-js` and `ci/lint-php` defaulted to the `v1.0.0/task/ci-lint-workflows` branch, which no longer exists, so a workflow scaffolded with the defaults failed before any job ran. `ci/test-measure` defaulted to the stale `release/v1.0.0` branch. The lint templates and the `wp-tooling list` descriptions drop their outdated "EXPERIMENTAL" wording. +- `ci/cd-wporg` works. It passed inputs and secrets `cd-wp-org.yml` never had (`plugin-slug`, `svn-username`, `svn-password`) and deployed an artifact nothing built, so it failed before any job ran. It now builds with `ci-build.yml`, packages with `wp-tooling release:zip` (honouring `.distignore`), and deploys with `cd-wp-org.yml`'s `slug`, `tag`, `artifact-name` and `WP_ORG_USERNAME`/`WP_ORG_PASSWORD`. New inputs: `wsw_ref`, `build_command`, `assets_path`. +- A Jest test renders every `ci/*` scaffold, with and without `node_version`, and checks each call's `with:` and `secrets:` keys against a snapshot of the wp-shared-workflows `v1` contracts (`tests/fixtures/wp-shared-workflows-v1-contracts.json`), so a mismatch fails here instead of in a project's first run. - `node_version` in the `ci/build`, `ci/lint-css`, `ci/lint-js`, `ci/test-a11y`, `ci/test-js` and `ci/test-php` scaffolds defaults to empty, which leaves `node-version` out of the generated workflow, so the shared workflow uses the project's `.nvmrc`. The old default of `22` overrode `.nvmrc`. Passing `--node_version` still pins a version. - `ci/test-measure`'s default WordPress versions include 7.1. diff --git a/node-packages/wp-tooling/scaffolds/ci/artifact-gate/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/artifact-gate/scaffold.json index 550cac2..05a1db4 100644 --- a/node-packages/wp-tooling/scaffolds/ci/artifact-gate/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/artifact-gate/scaffold.json @@ -2,7 +2,7 @@ "slug": "artifact-gate", "category": "ci", "name": "CI: Block committed build artifacts", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-build-artifact-gate.yml. Fails the PR if build outputs (assets/build/ by default) are committed. Stable (merged into release/v1.0.0).", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-build-artifact-gate.yml. Fails the PR if build outputs (assets/build/ by default) are committed. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json index db004e7..aff5dd6 100644 --- a/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/build/scaffold.json @@ -2,7 +2,7 @@ "slug": "build", "category": "ci", "name": "CI: production build", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-build.yml. Produces a production build (npm and/or composer) and optionally uploads the result as an artifact for downstream CD jobs. Stable (merged into release/v1.0.0).", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-build.yml. Produces a production build (npm and/or composer) and optionally uploads the result as an artifact for downstream CD jobs. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json index 758a36a..e553784 100644 --- a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json @@ -2,13 +2,28 @@ "slug": "cd-wporg", "category": "ci", "name": "WordPress.org deploy", - "description": "Deploys built plugin asset to WordPress.org SVN on each GitHub release. Wraps the wp-shared-workflows reusable.", + "description": "Builds and packages the plugin, then deploys it to WordPress.org SVN on each published GitHub release, with the wp-shared-workflows ci-build.yml and cd-wp-org.yml.", "source": "template", "inputs": [ { "key": "plugin_slug", "description": "WordPress.org plugin slug (must match the slug registered with .org).", "required": true + }, + { + "key": "wsw_ref", + "description": "Tag or ref of rtCamp/wp-shared-workflows to call. Pin a major tag such as `v1`, never a branch: branches move or get deleted.", + "default": "v1" + }, + { + "key": "build_command", + "description": "Builds the plugin and leaves exactly the files to deploy in dist/plugin (readme.txt at its root). The default builds, packages with `wp-tooling release:zip` (which honours .distignore) and unpacks the zip; python3 is used because self-hosted runners have no unzip.", + "default": "npm run build:prod && npx wp-tooling release:zip && python3 -m zipfile -e dist/*.zip dist/package && mv dist/package/*/ dist/plugin" + }, + { + "key": "assets_path", + "description": "Directory mapped to SVN /assets/ (banners, icons, screenshots). Skipped if it does not exist.", + "default": ".wordpress-org" } ], "files": [ diff --git a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache index a457a34..941b417 100644 --- a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache +++ b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/templates/cd-wporg.yml.mustache @@ -1,14 +1,38 @@ name: Deploy to WordPress.org +# Scaffolded by @rtcamp/wp-tooling. +# Builds and packages the plugin, then deploys it to WordPress.org SVN on each +# published GitHub release, with the reusable ci-build.yml and cd-wp-org.yml from +# rtCamp/wp-shared-workflows. +# +# The release tag (a leading v is stripped) must match readme.txt's Stable tag. +# Needs the WPORG_USERNAME and WPORG_PASSWORD repository secrets. + on: release: types: [published] +permissions: + contents: read + jobs: + build: + uses: rtCamp/wp-shared-workflows/.github/workflows/ci-build.yml@{{wsw_ref}} + with: + install-composer-deps: true + build-command: '{{build_command}}' + upload-artifact: true + artifact-name: wporg-package + artifact-path: dist/plugin + deploy: - uses: rtCamp/wp-shared-workflows/.github/workflows/cd-wp-org.yml@v1 + needs: build + uses: rtCamp/wp-shared-workflows/.github/workflows/cd-wp-org.yml@{{wsw_ref}} with: - plugin-slug: {{plugin_slug}} + slug: {{plugin_slug}} + tag: ${{ github.event.release.tag_name }} + artifact-name: wporg-package + assets-path: {{assets_path}} secrets: - svn-username: ${{ secrets.WPORG_USERNAME }} - svn-password: ${{ secrets.WPORG_PASSWORD }} + WP_ORG_USERNAME: ${{ secrets.WPORG_USERNAME }} + WP_ORG_PASSWORD: ${{ secrets.WPORG_PASSWORD }} diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json index 9ee189c..0a883ed 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-css/scaffold.json @@ -2,7 +2,7 @@ "slug": "lint-css", "category": "ci", "name": "CI: Lint CSS (Stylelint)", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-css.yml. Runs Stylelint over changed files in a PR, or the whole project on push. EXPERIMENTAL: the upstream workflow is currently in rtCamp/wp-shared-workflows PR #2 and has not yet shipped on `v1`.", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-css.yml. Runs Stylelint over changed files in a PR, or the whole project on push. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json index 33c3b14..a2bfbf6 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-js/scaffold.json @@ -2,7 +2,7 @@ "slug": "lint-js", "category": "ci", "name": "CI: Lint JS (ESLint)", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-js.yml. Runs ESLint over changed files in a PR, or the whole project on push. Optionally validates package.json shape first. EXPERIMENTAL: the upstream workflow is currently in rtCamp/wp-shared-workflows PR #2 and has not yet shipped on `v1`.", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-js.yml. Runs ESLint over changed files in a PR, or the whole project on push. Optionally validates package.json shape first. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json index 6d24b16..0848f64 100644 --- a/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/lint-php/scaffold.json @@ -2,7 +2,7 @@ "slug": "lint-php", "category": "ci", "name": "CI: Lint PHP (PHPCS + optional PHPStan)", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-php.yml. Runs PHPCS (and optionally PHPStan) over changed files in a PR, or the whole project on push. EXPERIMENTAL: the upstream workflow is currently in rtCamp/wp-shared-workflows PR #2 and has not yet shipped on `v1`. Pin `wsw_ref` to the PR branch or a SHA until the PR merges.", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-lint-php.yml. Runs PHPCS (and optionally PHPStan) over changed files in a PR, or the whole project on push. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json index 84fb89e..e5e7f96 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-a11y/scaffold.json @@ -2,7 +2,7 @@ "slug": "test-a11y", "category": "ci", "name": "CI: Accessibility (pa11y)", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-a11y.yml. Runs pa11y-ci WCAG2AA scans against a wp-env-booted site. Stable (merged into release/v1.0.0).", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-a11y.yml. Runs pa11y-ci WCAG2AA scans against a wp-env-booted site. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json index 3dc58be..c908d5c 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-js/scaffold.json @@ -2,7 +2,7 @@ "slug": "test-js", "category": "ci", "name": "CI: Jest", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-js.yml. Runs Jest with optional cache. Stable (merged into release/v1.0.0).", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-js.yml. Runs Jest with optional cache. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json index d1f5e2a..5ef3f37 100644 --- a/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/test-php/scaffold.json @@ -2,7 +2,7 @@ "slug": "test-php", "category": "ci", "name": "CI: PHPUnit", - "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-php.yml. Runs PHPUnit with optional @wordpress/env for full integration tests. Stable (merged into release/v1.0.0).", + "description": "GitHub Actions workflow that calls rtCamp/wp-shared-workflows ci-test-php.yml. Runs PHPUnit with optional @wordpress/env for full integration tests. Calls `@v1` by default.", "source": "template", "inputs": [ { diff --git a/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json b/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json new file mode 100644 index 0000000..d3e3f8b --- /dev/null +++ b/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json @@ -0,0 +1,602 @@ +{ + "_source": "rtCamp/wp-shared-workflows at v1 (79619c8), on.workflow_call inputs and secrets. Regenerate when v1 moves.", + "cd-built-branch.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "targets": { + "required": false + }, + "target-branch": { + "required": false + }, + "source-ref": { + "required": false + }, + "publish-paths": { + "required": false + }, + "verify-paths": { + "required": false + }, + "node-version": { + "required": false + }, + "php-version": { + "required": false + }, + "composer-flags": { + "required": false + }, + "build-command": { + "required": false + }, + "commit-subject": { + "required": false + }, + "allow-rewind": { + "required": false + }, + "dry-run": { + "required": false + }, + "retention-days": { + "required": false + } + }, + "secrets": { + "push-token": { + "required": false + } + } + }, + "cd-github-release.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "tag": { + "required": true + }, + "artifact-name": { + "required": true + }, + "slug": { + "required": false + }, + "changelog-path": { + "required": false + }, + "draft": { + "required": false + }, + "prerelease": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "cd-s3.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "tag": { + "required": true + }, + "artifact-name": { + "required": true + }, + "bucket": { + "required": true + }, + "prefix": { + "required": false + }, + "cloudfront-distribution-id": { + "required": false + }, + "region": { + "required": false + } + }, + "secrets": { + "AWS_ACCESS_KEY_ID": { + "required": true + }, + "AWS_SECRET_ACCESS_KEY": { + "required": true + } + } + }, + "cd-wp-org.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "slug": { + "required": true + }, + "tag": { + "required": true + }, + "artifact-name": { + "required": true + }, + "assets-path": { + "required": false + }, + "dry-run": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": { + "WP_ORG_USERNAME": { + "required": true + }, + "WP_ORG_PASSWORD": { + "required": true + } + } + }, + "ci-build-artifact-gate.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "gated-paths": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-build.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "install-node-deps": { + "required": false + }, + "node-version": { + "required": false + }, + "install-composer-deps": { + "required": false + }, + "php-version": { + "required": false + }, + "composer-flags": { + "required": false + }, + "build-command": { + "required": false + }, + "upload-artifact": { + "required": false + }, + "artifact-name": { + "required": false + }, + "artifact-path": { + "required": false + }, + "retention-days": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-detect-changes.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "ignore-paths": { + "required": false + }, + "base-ref": { + "required": false + }, + "working-dir": { + "required": false + }, + "wp-tooling-ref": { + "required": false + } + }, + "secrets": {} + }, + "ci-lint-css.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "lint-command": { + "required": false + }, + "lint-files-command": { + "required": false + }, + "changed-files": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-lint-js.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "lint-command": { + "required": false + }, + "lint-files-command": { + "required": false + }, + "changed-files": { + "required": false + }, + "validate-package-json": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-lint-php.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "php-version": { + "required": false + }, + "enable-phpstan": { + "required": false + }, + "phpcs-command": { + "required": false + }, + "changed-files": { + "required": false + }, + "phpstan-command": { + "required": false + }, + "phpstan-level": { + "required": false + }, + "composer-flags": { + "required": false + }, + "validate-composer": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-test-a11y.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "build-command": { + "required": false + }, + "test-command": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-test-build-artifact.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "artifact-name": { + "required": true + }, + "slug": { + "required": true + }, + "node-version": { + "required": false + }, + "php-version": { + "required": false + }, + "wp-version": { + "required": false + }, + "install-path": { + "required": false + }, + "smoke-script": { + "required": false + }, + "doctor-ignore-checks": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-test-js.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "test-command": { + "required": false + }, + "enable-cache": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "ci-test-php.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "php-version": { + "required": false + }, + "node-version": { + "required": false + }, + "wp-version": { + "required": false + }, + "use-wp-env": { + "required": false + }, + "test-command": { + "required": false + }, + "composer-flags": { + "required": false + }, + "working-dir": { + "required": false + } + }, + "secrets": {} + }, + "version-monitor.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "node-version": { + "required": false + }, + "base-branch": { + "required": false + }, + "pr-label": { + "required": false + }, + "pr-assignees": { + "required": false + }, + "wp-tooling-ref": { + "required": false + } + }, + "secrets": {} + }, + "wp-cd.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "tag": { + "required": true + }, + "artifact-name": { + "required": true + }, + "deploy-target": { + "required": true + }, + "slug": { + "required": false + }, + "working-dir": { + "required": false + }, + "changelog-path": { + "required": false + }, + "draft": { + "required": false + }, + "prerelease": { + "required": false + }, + "assets-path": { + "required": false + }, + "dry-run": { + "required": false + }, + "bucket": { + "required": false + }, + "prefix": { + "required": false + }, + "cloudfront-distribution-id": { + "required": false + }, + "region": { + "required": false + } + }, + "secrets": { + "WP_ORG_USERNAME": { + "required": false + }, + "WP_ORG_PASSWORD": { + "required": false + }, + "AWS_ACCESS_KEY_ID": { + "required": false + }, + "AWS_SECRET_ACCESS_KEY": { + "required": false + } + } + }, + "wp-ci.yml": { + "inputs": { + "runs-on": { + "required": false + }, + "project-type": { + "required": true + }, + "working-dir": { + "required": false + }, + "skip": { + "required": false + }, + "enable-phpstan": { + "required": false + }, + "run-a11y": { + "required": false + }, + "use-wp-env": { + "required": false + }, + "php-versions": { + "required": false + }, + "wp-versions": { + "required": false + }, + "test-php-exclude": { + "required": false + }, + "build-artifact-path": { + "required": false + }, + "build-artifact-slug": { + "required": false + }, + "ignore-paths": { + "required": false + }, + "base-ref": { + "required": false + }, + "wp-tooling-ref": { + "required": false + }, + "gated-paths": { + "required": false + }, + "node-version": { + "required": false + }, + "php-version": { + "required": false + }, + "composer-flags": { + "required": false + }, + "build-composer-flags": { + "required": false + }, + "install-composer-deps": { + "required": false + }, + "build-command": { + "required": false + }, + "lint-css-command": { + "required": false + }, + "lint-css-files-command": { + "required": false + }, + "lint-js-command": { + "required": false + }, + "lint-js-files-command": { + "required": false + }, + "phpcs-command": { + "required": false + }, + "phpstan-command": { + "required": false + }, + "test-js-command": { + "required": false + }, + "test-js-cache": { + "required": false + }, + "test-php-command": { + "required": false + }, + "a11y-command": { + "required": false + } + }, + "secrets": {} + } +} diff --git a/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js b/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js new file mode 100644 index 0000000..736a7fb --- /dev/null +++ b/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js @@ -0,0 +1,222 @@ +/** + * Renders every bundled `ci/*` scaffold and checks the generated workflow + * against the reusable workflow it calls in rtCamp/wp-shared-workflows. + * + * actionlint cannot see a remote reusable workflow's inputs, so a template + * that passes a misspelt input, or leaves out a required one, renders a file + * that lints clean and then fails before any job starts. These tests compare + * each job's `with:` and `secrets:` keys with a snapshot of the `v1` + * workflow_call contracts (tests/fixtures/wp-shared-workflows-v1-contracts.json), + * and catch an empty `with:` block, which an optional input left out by its + * section can produce. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { ScaffoldRegistry } = require('../../src/scaffolds/registry'); + +const DEFAULTS_DIR = path.join(__dirname, '..', '..', 'scaffolds'); +const CI_DIR = path.join(DEFAULTS_DIR, 'ci'); +const CONTRACTS = require('../fixtures/wp-shared-workflows-v1-contracts.json'); + +const USES_RE = + /^rtCamp\/wp-shared-workflows\/\.github\/workflows\/([\w.-]+\.yml)@(.+)$/; + +const tempDirs = []; + +function makeTmpDir() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wp-tooling-ci-')); + tempDirs.push(dir); + return dir; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +let registry; +beforeAll(async () => { + registry = new ScaffoldRegistry({ defaultsDir: DEFAULTS_DIR }); + await registry.scan(); +}); + +/** + * Every bundled ci/* scaffold, with values for its required inputs and + * whether it declares node_version. + */ +const CI_SCAFFOLDS = fs + .readdirSync(CI_DIR) + .filter((name) => fs.existsSync(path.join(CI_DIR, name, 'scaffold.json'))) + .sort() + .map((name) => { + const manifest = JSON.parse( + fs.readFileSync(path.join(CI_DIR, name, 'scaffold.json'), 'utf8') + ); + const required = {}; + for (const input of manifest.inputs || []) { + if (input.required && undefined === input.default) { + required[input.key] = 'acme-plugin'; + } + } + const hasNodeVersion = (manifest.inputs || []).some( + (input) => 'node_version' === input.key + ); + return { id: `ci/${name}`, required, hasNodeVersion }; + }); + +/** + * Reads the jobs that call a reusable workflow, with the keys under their + * `with:` and `secrets:` blocks. The templates are plain block YAML, so + * indentation is enough to find them. + * + * @param {string} yaml Rendered workflow. + * @return {Array} One entry per calling job. + */ +function readCalls(yaml) { + const lines = yaml.split('\n'); + const calls = []; + let current = null; + let block = null; + let blockIndent = 0; + + for (const line of lines) { + if (!line.trim() || line.trim().startsWith('#')) { + continue; + } + const indent = line.length - line.trimStart().length; + const text = line.trim(); + + if (block && indent <= blockIndent) { + block = null; + } + + const uses = text.match(/^uses:\s*(\S+)$/); + if (uses && 4 === indent) { + current = { uses: uses[1], with: [], secrets: [] }; + calls.push(current); + continue; + } + if (current && 4 === indent && /^(with|secrets):$/.test(text)) { + block = text.slice(0, -1); + blockIndent = indent; + continue; + } + if (current && block && indent === blockIndent + 2) { + current[block].push(text.split(':')[0]); + } + if (2 === indent) { + current = null; + block = null; + } + } + return calls; +} + +async function renderAll(id, inputs) { + const cwd = makeTmpDir(); + await registry.execute(id, inputs, { dryRun: false, cwd }); + const dir = path.join(cwd, '.github', 'workflows'); + return fs + .readdirSync(dir) + .map((file) => fs.readFileSync(path.join(dir, file), 'utf8')); +} + +describe('bundled ci/* scaffolds render workflows that match wp-shared-workflows v1', () => { + it('finds the ci scaffolds', () => { + expect(CI_SCAFFOLDS.length).toBeGreaterThan(5); + }); + + const cases = []; + for (const scaffold of CI_SCAFFOLDS) { + cases.push([scaffold.id, 'defaults', scaffold.required]); + if (scaffold.hasNodeVersion) { + cases.push([ + scaffold.id, + 'node_version 24', + { ...scaffold.required, node_version: '24' }, + ]); + } + } + + it.each(cases)('%s with %s', async (id, _label, inputs) => { + const files = await renderAll(id, inputs); + expect(files.length).toBeGreaterThan(0); + + for (const yaml of files) { + // No unrendered placeholder or section tag. + expect(yaml).not.toMatch(/\{\{[#^/]?\w+\}\}/); + + // No `with:` or `secrets:` followed by nothing at a deeper indent. + const lines = yaml.split('\n').filter((line) => line.trim()); + const emptyBlocks = lines.filter((line, i) => { + if (!/^\s*(with|secrets):$/.test(line)) { + return false; + } + const indent = line.length - line.trimStart().length; + const next = lines[i + 1] || ''; + return next.length - next.trimStart().length <= indent; + }); + expect(emptyBlocks).toEqual([]); + + const calls = readCalls(yaml); + expect(calls.length).toBeGreaterThan(0); + + for (const call of calls) { + const match = call.uses.match(USES_RE); + expect(match).not.toBeNull(); + const [, workflow, ref] = match; + expect(ref).toBe('v1'); + + const contract = CONTRACTS[workflow]; + expect(contract).toBeDefined(); + + const unknownInputs = call.with.filter( + (key) => !(key in contract.inputs) + ); + const unknownSecrets = call.secrets.filter( + (key) => !(key in contract.secrets) + ); + const missingInputs = Object.keys(contract.inputs).filter( + (key) => + contract.inputs[key].required && + !call.with.includes(key) + ); + const missingSecrets = Object.keys(contract.secrets).filter( + (key) => + contract.secrets[key].required && + !call.secrets.includes(key) + ); + expect({ + workflow, + unknownInputs, + unknownSecrets, + missingInputs, + missingSecrets, + }).toEqual({ + workflow, + unknownInputs: [], + unknownSecrets: [], + missingInputs: [], + missingSecrets: [], + }); + } + } + }); + + it('leaves node-version out by default and sets it when node_version is given', async () => { + for (const scaffold of CI_SCAFFOLDS.filter((s) => s.hasNodeVersion)) { + const [byDefault] = await renderAll(scaffold.id, scaffold.required); + expect(byDefault).not.toContain('node-version:'); + + const [pinned] = await renderAll(scaffold.id, { + ...scaffold.required, + node_version: '24', + }); + expect(pinned).toContain('node-version: "24"'); + } + }); +}); From c0fcb5c96344c813d86f661101fd20ed301ec0ed Mon Sep 17 00:00:00 2001 From: Aryan Jasala Date: Tue, 10 Nov 2026 00:32:03 +0530 Subject: [PATCH 3/3] fix(scaffolds): run release:zip through the scoped package name npx falls back to the registry when the project has no local install, and nobody owns the unscoped wp-tooling name there. Also drop the v1 contract test and its fixture: it compared the templates with a hand-copied snapshot, so it could not catch upstream changes, and wp-shared-workflows already checks its own callers in CI. --- node-packages/wp-tooling/CHANGELOG.md | 3 +- .../scaffolds/ci/cd-wporg/scaffold.json | 2 +- .../wp-shared-workflows-v1-contracts.json | 602 ------------------ .../tests/scaffolds/ci-workflows.test.js | 222 ------- 4 files changed, 2 insertions(+), 827 deletions(-) delete mode 100644 node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json delete mode 100644 node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js diff --git a/node-packages/wp-tooling/CHANGELOG.md b/node-packages/wp-tooling/CHANGELOG.md index e392b2f..8cdbce5 100644 --- a/node-packages/wp-tooling/CHANGELOG.md +++ b/node-packages/wp-tooling/CHANGELOG.md @@ -9,8 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - The CI scaffolds call rtCamp/wp-shared-workflows at `@v1`. `ci/lint-css`, `ci/lint-js` and `ci/lint-php` defaulted to the `v1.0.0/task/ci-lint-workflows` branch, which no longer exists, so a workflow scaffolded with the defaults failed before any job ran. `ci/test-measure` defaulted to the stale `release/v1.0.0` branch. The lint templates and the `wp-tooling list` descriptions drop their outdated "EXPERIMENTAL" wording. -- `ci/cd-wporg` works. It passed inputs and secrets `cd-wp-org.yml` never had (`plugin-slug`, `svn-username`, `svn-password`) and deployed an artifact nothing built, so it failed before any job ran. It now builds with `ci-build.yml`, packages with `wp-tooling release:zip` (honouring `.distignore`), and deploys with `cd-wp-org.yml`'s `slug`, `tag`, `artifact-name` and `WP_ORG_USERNAME`/`WP_ORG_PASSWORD`. New inputs: `wsw_ref`, `build_command`, `assets_path`. -- A Jest test renders every `ci/*` scaffold, with and without `node_version`, and checks each call's `with:` and `secrets:` keys against a snapshot of the wp-shared-workflows `v1` contracts (`tests/fixtures/wp-shared-workflows-v1-contracts.json`), so a mismatch fails here instead of in a project's first run. +- `ci/cd-wporg` works. It passed inputs and secrets `cd-wp-org.yml` never had (`plugin-slug`, `svn-username`, `svn-password`) and deployed an artifact nothing built, so it failed before any job ran. It now builds with `ci-build.yml`, packages with `npx @rtcamp/wp-tooling release:zip` (honouring `.distignore`; the scoped name, so npx never fetches an unrelated `wp-tooling` package from the registry), and deploys with `cd-wp-org.yml`'s `slug`, `tag`, `artifact-name` and `WP_ORG_USERNAME`/`WP_ORG_PASSWORD`. New inputs: `wsw_ref`, `build_command`, `assets_path`. - `node_version` in the `ci/build`, `ci/lint-css`, `ci/lint-js`, `ci/test-a11y`, `ci/test-js` and `ci/test-php` scaffolds defaults to empty, which leaves `node-version` out of the generated workflow, so the shared workflow uses the project's `.nvmrc`. The old default of `22` overrode `.nvmrc`. Passing `--node_version` still pins a version. - `ci/test-measure`'s default WordPress versions include 7.1. diff --git a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json index e553784..86963b9 100644 --- a/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/ci/cd-wporg/scaffold.json @@ -18,7 +18,7 @@ { "key": "build_command", "description": "Builds the plugin and leaves exactly the files to deploy in dist/plugin (readme.txt at its root). The default builds, packages with `wp-tooling release:zip` (which honours .distignore) and unpacks the zip; python3 is used because self-hosted runners have no unzip.", - "default": "npm run build:prod && npx wp-tooling release:zip && python3 -m zipfile -e dist/*.zip dist/package && mv dist/package/*/ dist/plugin" + "default": "npm run build:prod && npx @rtcamp/wp-tooling release:zip && python3 -m zipfile -e dist/*.zip dist/package && mv dist/package/*/ dist/plugin" }, { "key": "assets_path", diff --git a/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json b/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json deleted file mode 100644 index d3e3f8b..0000000 --- a/node-packages/wp-tooling/tests/fixtures/wp-shared-workflows-v1-contracts.json +++ /dev/null @@ -1,602 +0,0 @@ -{ - "_source": "rtCamp/wp-shared-workflows at v1 (79619c8), on.workflow_call inputs and secrets. Regenerate when v1 moves.", - "cd-built-branch.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "targets": { - "required": false - }, - "target-branch": { - "required": false - }, - "source-ref": { - "required": false - }, - "publish-paths": { - "required": false - }, - "verify-paths": { - "required": false - }, - "node-version": { - "required": false - }, - "php-version": { - "required": false - }, - "composer-flags": { - "required": false - }, - "build-command": { - "required": false - }, - "commit-subject": { - "required": false - }, - "allow-rewind": { - "required": false - }, - "dry-run": { - "required": false - }, - "retention-days": { - "required": false - } - }, - "secrets": { - "push-token": { - "required": false - } - } - }, - "cd-github-release.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "tag": { - "required": true - }, - "artifact-name": { - "required": true - }, - "slug": { - "required": false - }, - "changelog-path": { - "required": false - }, - "draft": { - "required": false - }, - "prerelease": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "cd-s3.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "tag": { - "required": true - }, - "artifact-name": { - "required": true - }, - "bucket": { - "required": true - }, - "prefix": { - "required": false - }, - "cloudfront-distribution-id": { - "required": false - }, - "region": { - "required": false - } - }, - "secrets": { - "AWS_ACCESS_KEY_ID": { - "required": true - }, - "AWS_SECRET_ACCESS_KEY": { - "required": true - } - } - }, - "cd-wp-org.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "slug": { - "required": true - }, - "tag": { - "required": true - }, - "artifact-name": { - "required": true - }, - "assets-path": { - "required": false - }, - "dry-run": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": { - "WP_ORG_USERNAME": { - "required": true - }, - "WP_ORG_PASSWORD": { - "required": true - } - } - }, - "ci-build-artifact-gate.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "gated-paths": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-build.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "install-node-deps": { - "required": false - }, - "node-version": { - "required": false - }, - "install-composer-deps": { - "required": false - }, - "php-version": { - "required": false - }, - "composer-flags": { - "required": false - }, - "build-command": { - "required": false - }, - "upload-artifact": { - "required": false - }, - "artifact-name": { - "required": false - }, - "artifact-path": { - "required": false - }, - "retention-days": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-detect-changes.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "ignore-paths": { - "required": false - }, - "base-ref": { - "required": false - }, - "working-dir": { - "required": false - }, - "wp-tooling-ref": { - "required": false - } - }, - "secrets": {} - }, - "ci-lint-css.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "lint-command": { - "required": false - }, - "lint-files-command": { - "required": false - }, - "changed-files": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-lint-js.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "lint-command": { - "required": false - }, - "lint-files-command": { - "required": false - }, - "changed-files": { - "required": false - }, - "validate-package-json": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-lint-php.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "php-version": { - "required": false - }, - "enable-phpstan": { - "required": false - }, - "phpcs-command": { - "required": false - }, - "changed-files": { - "required": false - }, - "phpstan-command": { - "required": false - }, - "phpstan-level": { - "required": false - }, - "composer-flags": { - "required": false - }, - "validate-composer": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-test-a11y.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "build-command": { - "required": false - }, - "test-command": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-test-build-artifact.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "artifact-name": { - "required": true - }, - "slug": { - "required": true - }, - "node-version": { - "required": false - }, - "php-version": { - "required": false - }, - "wp-version": { - "required": false - }, - "install-path": { - "required": false - }, - "smoke-script": { - "required": false - }, - "doctor-ignore-checks": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-test-js.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "test-command": { - "required": false - }, - "enable-cache": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "ci-test-php.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "php-version": { - "required": false - }, - "node-version": { - "required": false - }, - "wp-version": { - "required": false - }, - "use-wp-env": { - "required": false - }, - "test-command": { - "required": false - }, - "composer-flags": { - "required": false - }, - "working-dir": { - "required": false - } - }, - "secrets": {} - }, - "version-monitor.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "node-version": { - "required": false - }, - "base-branch": { - "required": false - }, - "pr-label": { - "required": false - }, - "pr-assignees": { - "required": false - }, - "wp-tooling-ref": { - "required": false - } - }, - "secrets": {} - }, - "wp-cd.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "tag": { - "required": true - }, - "artifact-name": { - "required": true - }, - "deploy-target": { - "required": true - }, - "slug": { - "required": false - }, - "working-dir": { - "required": false - }, - "changelog-path": { - "required": false - }, - "draft": { - "required": false - }, - "prerelease": { - "required": false - }, - "assets-path": { - "required": false - }, - "dry-run": { - "required": false - }, - "bucket": { - "required": false - }, - "prefix": { - "required": false - }, - "cloudfront-distribution-id": { - "required": false - }, - "region": { - "required": false - } - }, - "secrets": { - "WP_ORG_USERNAME": { - "required": false - }, - "WP_ORG_PASSWORD": { - "required": false - }, - "AWS_ACCESS_KEY_ID": { - "required": false - }, - "AWS_SECRET_ACCESS_KEY": { - "required": false - } - } - }, - "wp-ci.yml": { - "inputs": { - "runs-on": { - "required": false - }, - "project-type": { - "required": true - }, - "working-dir": { - "required": false - }, - "skip": { - "required": false - }, - "enable-phpstan": { - "required": false - }, - "run-a11y": { - "required": false - }, - "use-wp-env": { - "required": false - }, - "php-versions": { - "required": false - }, - "wp-versions": { - "required": false - }, - "test-php-exclude": { - "required": false - }, - "build-artifact-path": { - "required": false - }, - "build-artifact-slug": { - "required": false - }, - "ignore-paths": { - "required": false - }, - "base-ref": { - "required": false - }, - "wp-tooling-ref": { - "required": false - }, - "gated-paths": { - "required": false - }, - "node-version": { - "required": false - }, - "php-version": { - "required": false - }, - "composer-flags": { - "required": false - }, - "build-composer-flags": { - "required": false - }, - "install-composer-deps": { - "required": false - }, - "build-command": { - "required": false - }, - "lint-css-command": { - "required": false - }, - "lint-css-files-command": { - "required": false - }, - "lint-js-command": { - "required": false - }, - "lint-js-files-command": { - "required": false - }, - "phpcs-command": { - "required": false - }, - "phpstan-command": { - "required": false - }, - "test-js-command": { - "required": false - }, - "test-js-cache": { - "required": false - }, - "test-php-command": { - "required": false - }, - "a11y-command": { - "required": false - } - }, - "secrets": {} - } -} diff --git a/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js b/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js deleted file mode 100644 index 736a7fb..0000000 --- a/node-packages/wp-tooling/tests/scaffolds/ci-workflows.test.js +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Renders every bundled `ci/*` scaffold and checks the generated workflow - * against the reusable workflow it calls in rtCamp/wp-shared-workflows. - * - * actionlint cannot see a remote reusable workflow's inputs, so a template - * that passes a misspelt input, or leaves out a required one, renders a file - * that lints clean and then fails before any job starts. These tests compare - * each job's `with:` and `secrets:` keys with a snapshot of the `v1` - * workflow_call contracts (tests/fixtures/wp-shared-workflows-v1-contracts.json), - * and catch an empty `with:` block, which an optional input left out by its - * section can produce. - */ - -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -const { ScaffoldRegistry } = require('../../src/scaffolds/registry'); - -const DEFAULTS_DIR = path.join(__dirname, '..', '..', 'scaffolds'); -const CI_DIR = path.join(DEFAULTS_DIR, 'ci'); -const CONTRACTS = require('../fixtures/wp-shared-workflows-v1-contracts.json'); - -const USES_RE = - /^rtCamp\/wp-shared-workflows\/\.github\/workflows\/([\w.-]+\.yml)@(.+)$/; - -const tempDirs = []; - -function makeTmpDir() { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wp-tooling-ci-')); - tempDirs.push(dir); - return dir; -} - -afterEach(() => { - for (const dir of tempDirs.splice(0)) { - fs.rmSync(dir, { recursive: true, force: true }); - } -}); - -let registry; -beforeAll(async () => { - registry = new ScaffoldRegistry({ defaultsDir: DEFAULTS_DIR }); - await registry.scan(); -}); - -/** - * Every bundled ci/* scaffold, with values for its required inputs and - * whether it declares node_version. - */ -const CI_SCAFFOLDS = fs - .readdirSync(CI_DIR) - .filter((name) => fs.existsSync(path.join(CI_DIR, name, 'scaffold.json'))) - .sort() - .map((name) => { - const manifest = JSON.parse( - fs.readFileSync(path.join(CI_DIR, name, 'scaffold.json'), 'utf8') - ); - const required = {}; - for (const input of manifest.inputs || []) { - if (input.required && undefined === input.default) { - required[input.key] = 'acme-plugin'; - } - } - const hasNodeVersion = (manifest.inputs || []).some( - (input) => 'node_version' === input.key - ); - return { id: `ci/${name}`, required, hasNodeVersion }; - }); - -/** - * Reads the jobs that call a reusable workflow, with the keys under their - * `with:` and `secrets:` blocks. The templates are plain block YAML, so - * indentation is enough to find them. - * - * @param {string} yaml Rendered workflow. - * @return {Array} One entry per calling job. - */ -function readCalls(yaml) { - const lines = yaml.split('\n'); - const calls = []; - let current = null; - let block = null; - let blockIndent = 0; - - for (const line of lines) { - if (!line.trim() || line.trim().startsWith('#')) { - continue; - } - const indent = line.length - line.trimStart().length; - const text = line.trim(); - - if (block && indent <= blockIndent) { - block = null; - } - - const uses = text.match(/^uses:\s*(\S+)$/); - if (uses && 4 === indent) { - current = { uses: uses[1], with: [], secrets: [] }; - calls.push(current); - continue; - } - if (current && 4 === indent && /^(with|secrets):$/.test(text)) { - block = text.slice(0, -1); - blockIndent = indent; - continue; - } - if (current && block && indent === blockIndent + 2) { - current[block].push(text.split(':')[0]); - } - if (2 === indent) { - current = null; - block = null; - } - } - return calls; -} - -async function renderAll(id, inputs) { - const cwd = makeTmpDir(); - await registry.execute(id, inputs, { dryRun: false, cwd }); - const dir = path.join(cwd, '.github', 'workflows'); - return fs - .readdirSync(dir) - .map((file) => fs.readFileSync(path.join(dir, file), 'utf8')); -} - -describe('bundled ci/* scaffolds render workflows that match wp-shared-workflows v1', () => { - it('finds the ci scaffolds', () => { - expect(CI_SCAFFOLDS.length).toBeGreaterThan(5); - }); - - const cases = []; - for (const scaffold of CI_SCAFFOLDS) { - cases.push([scaffold.id, 'defaults', scaffold.required]); - if (scaffold.hasNodeVersion) { - cases.push([ - scaffold.id, - 'node_version 24', - { ...scaffold.required, node_version: '24' }, - ]); - } - } - - it.each(cases)('%s with %s', async (id, _label, inputs) => { - const files = await renderAll(id, inputs); - expect(files.length).toBeGreaterThan(0); - - for (const yaml of files) { - // No unrendered placeholder or section tag. - expect(yaml).not.toMatch(/\{\{[#^/]?\w+\}\}/); - - // No `with:` or `secrets:` followed by nothing at a deeper indent. - const lines = yaml.split('\n').filter((line) => line.trim()); - const emptyBlocks = lines.filter((line, i) => { - if (!/^\s*(with|secrets):$/.test(line)) { - return false; - } - const indent = line.length - line.trimStart().length; - const next = lines[i + 1] || ''; - return next.length - next.trimStart().length <= indent; - }); - expect(emptyBlocks).toEqual([]); - - const calls = readCalls(yaml); - expect(calls.length).toBeGreaterThan(0); - - for (const call of calls) { - const match = call.uses.match(USES_RE); - expect(match).not.toBeNull(); - const [, workflow, ref] = match; - expect(ref).toBe('v1'); - - const contract = CONTRACTS[workflow]; - expect(contract).toBeDefined(); - - const unknownInputs = call.with.filter( - (key) => !(key in contract.inputs) - ); - const unknownSecrets = call.secrets.filter( - (key) => !(key in contract.secrets) - ); - const missingInputs = Object.keys(contract.inputs).filter( - (key) => - contract.inputs[key].required && - !call.with.includes(key) - ); - const missingSecrets = Object.keys(contract.secrets).filter( - (key) => - contract.secrets[key].required && - !call.secrets.includes(key) - ); - expect({ - workflow, - unknownInputs, - unknownSecrets, - missingInputs, - missingSecrets, - }).toEqual({ - workflow, - unknownInputs: [], - unknownSecrets: [], - missingInputs: [], - missingSecrets: [], - }); - } - } - }); - - it('leaves node-version out by default and sets it when node_version is given', async () => { - for (const scaffold of CI_SCAFFOLDS.filter((s) => s.hasNodeVersion)) { - const [byDefault] = await renderAll(scaffold.id, scaffold.required); - expect(byDefault).not.toContain('node-version:'); - - const [pinned] = await renderAll(scaffold.id, { - ...scaffold.required, - node_version: '24', - }); - expect(pinned).toContain('node-version: "24"'); - } - }); -});