From 8f19c0338d8855c0ba611009cb19fa89cdf44e06 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Mon, 5 Oct 2026 22:23:45 +0530 Subject: [PATCH 1/2] feat(ci): forward inputs through wp-ci, resolve .nvmrc --- .github/workflows/cd-built-branch.yml | 16 +++- .github/workflows/ci-build.yml | 42 +++++++--- .github/workflows/ci-lint-css.yml | 61 +++++++++++---- .github/workflows/ci-lint-js.yml | 59 ++++++++++++--- .github/workflows/ci-lint-php.yml | 26 ++++--- .github/workflows/ci-test-a11y.yml | 39 +++++++--- .github/workflows/ci-test-build-artifact.yml | 33 ++++++-- .github/workflows/ci-test-js.yml | 37 +++++++-- .github/workflows/ci-test-php.yml | 34 +++++++-- .github/workflows/wp-ci.yml | 80 ++++++++++++++++++++ AGENTS.md | 2 + README.md | 4 + 12 files changed, 353 insertions(+), 80 deletions(-) diff --git a/.github/workflows/cd-built-branch.yml b/.github/workflows/cd-built-branch.yml index 1f29a89..f25e24e 100644 --- a/.github/workflows/cd-built-branch.yml +++ b/.github/workflows/cd-built-branch.yml @@ -51,9 +51,9 @@ on: type: string default: "" node-version: - description: "Default Node.js version for every target, overridable per target." + description: "Default Node.js version for every target, overridable per target. Leave empty to read the target's .nvmrc, then the repo root's, falling back to 22." type: string - default: "22" + default: "" php-version: description: "Default PHP version for every target, overridable per target." type: string @@ -230,6 +230,13 @@ jobs: php_version="$(jq -r --arg d "$INPUT_PHP_VERSION" '.["php-version"] // $d' <<<"$item")" node_version="$(jq -r --arg d "$INPUT_NODE_VERSION" '.["node-version"] // $d' <<<"$item")" + node_version_file="" + if [ -z "$node_version" ]; then + if [ -s "$dir/.nvmrc" ]; then node_version_file="${dir%/}/.nvmrc" + elif [ -s .nvmrc ]; then node_version_file=".nvmrc" + else node_version="22" + fi + fi composer_flags="$(jq -r --arg d "$INPUT_COMPOSER_FLAGS" '.["composer-flags"] // $d' <<<"$item")" build_command="$(jq -r --arg d "$INPUT_BUILD_COMMAND" '.["build-command"] // $d' <<<"$item")" [ -n "$build_command" ] || build_command="npm run build:prod" @@ -254,12 +261,12 @@ jobs: entry="$(jq -nc \ --arg name "$name" --arg dir "$dir" \ --arg composer "$composer" --arg node "$node" \ - --arg php "$php_version" --arg nodever "$node_version" \ + --arg php "$php_version" --arg nodever "$node_version" --arg nodefile "$node_version_file" \ --arg cflags "$composer_flags" --arg bcmd "$build_command" \ --arg lock "${dir%/}/package-lock.json" \ --arg pp "$publish_abs" --arg vp "$verify_abs" \ '{name:$name, dir:$dir, composer:$composer, node:$node, - "php-version":$php, "node-version":$nodever, + "php-version":$php, "node-version":$nodever, "node-version-file":$nodefile, "composer-flags":$cflags, "build-command":$bcmd, lockfile:$lock, "publish-paths":$pp, "verify-paths":$vp}')" items="$(jq -c --argjson e "$entry" '. + [$e]' <<<"$items")" @@ -332,6 +339,7 @@ jobs: uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: ${{ matrix.node-version }} + node-version-file: ${{ matrix.node-version-file }} cache: npm # setup-node is a JS action and ignores defaults.run.working-directory, so without # this it looks for the lockfile at the repo root. diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 6ab8ea2..644d544 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -12,21 +12,23 @@ on: type: boolean default: true node-version: - description: "Node.js version (used only when install-node-deps is true)." + description: "Node.js version, used only when install-node-deps is true. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" install-composer-deps: description: "Install Composer dependencies before the build." type: boolean default: false php-version: - description: "PHP version (used only when install-composer-deps is true)." + description: "PHP version, used only when install-composer-deps is true. Leave empty for 8.3." type: string - default: "8.3" + default: "" composer-flags: - description: "Flags passed to composer install." + description: >- + Flags passed to composer install. Leave empty for '--no-dev --prefer-dist + --optimize-autoloader --no-progress --no-interaction --no-scripts'. type: string - default: "--no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --no-scripts" + default: "" build-command: description: "Shell command that produces the production build. When empty, defaults to 'npm run build:prod' if install-node-deps is true, otherwise 'composer run-script build'." type: string @@ -66,11 +68,33 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + if: ${{ inputs.install-node-deps }} + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js if: ${{ inputs.install-node-deps }} uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -79,7 +103,7 @@ jobs: if: ${{ inputs.install-composer-deps }} uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: - php-version: ${{ inputs.php-version }} + php-version: ${{ inputs.php-version || '8.3' }} coverage: none tools: composer @@ -87,7 +111,7 @@ jobs: if: ${{ inputs.install-composer-deps }} shell: bash env: - COMPOSER_FLAGS: ${{ inputs.composer-flags }} + COMPOSER_FLAGS: ${{ inputs.composer-flags || '--no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --no-scripts' }} run: | set -euo pipefail read -r -a composer_args <<< "$COMPOSER_FLAGS" || true diff --git a/.github/workflows/ci-lint-css.yml b/.github/workflows/ci-lint-css.yml index 772bb04..b12d811 100644 --- a/.github/workflows/ci-lint-css.yml +++ b/.github/workflows/ci-lint-css.yml @@ -8,15 +8,20 @@ on: type: string default: '"ubuntu-latest"' node-version: - description: "Node.js version." + description: "Node.js version. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" lint-command: - description: "Shell command that runs Stylelint over the whole project. Used when changed-files is empty." + description: >- + Shell command that runs Stylelint. The changed files are appended as arguments, so a + custom command must pass them on, as npm run does. Leave empty for 'npm run lint:css'. type: string - default: "npm run lint:css" + default: "" changed-files: - description: "Newline-separated changed files (ci-detect-changes' css-files). Stylesheets are linted file by file; if the list is empty or holds anything else (package.json, package-lock.json), lint-command runs over the whole project." + description: >- + Newline-separated changed files (ci-detect-changes' css-files). Only stylesheets that + still exist are linted, and a list of only deleted stylesheets skips Stylelint. No list, + or a change to package.json or package-lock.json, lints the whole project. type: string default: "" working-dir: @@ -38,10 +43,31 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -53,15 +79,20 @@ jobs: shell: bash env: CHANGED_FILES: ${{ inputs.changed-files }} - LINT_COMMAND: ${{ inputs.lint-command }} + LINT_COMMAND: ${{ inputs.lint-command || 'npm run lint:css' }} run: | - # css-files also lists package.json and package-lock.json, so a dependency - # change re-runs this job. Stylelint cannot parse them, and a dependency - # change can affect every stylesheet, so lint the whole project then. - stylesheets=$(printf '%s\n' "$CHANGED_FILES" | grep -E '\.s?css$' || true) + set -eo pipefail + # Lint only the changed stylesheets that still exist; "./" keeps a name like -x.css a path. + # No list, or a package.json/lockfile change, lints the whole project. + stylesheets=$(printf '%s\n' "$CHANGED_FILES" | grep -E '\.s?css$' \ + | while IFS= read -r file; do [ ! -f "$file" ] || printf './%s\n' "$file"; done || true) others=$(printf '%s\n' "$CHANGED_FILES" | grep -vE '^$|\.s?css$' || true) - if [ -n "$stylesheets" ] && [ -z "$others" ]; then - printf '%s\n' "$stylesheets" | xargs -d '\n' --no-run-if-empty npx --no-install stylelint -- - else - bash -eo pipefail -c "$LINT_COMMAND" + files=() + if [ -n "$CHANGED_FILES" ] && [ -z "$others" ]; then + if [ -z "$stylesheets" ]; then + echo "::notice::Nothing to lint: the changed stylesheets were deleted." + exit 0 + fi + mapfile -t files <<< "$stylesheets" fi + bash -eo pipefail -c "$LINT_COMMAND \"\$@\"" _ "${files[@]}" diff --git a/.github/workflows/ci-lint-js.yml b/.github/workflows/ci-lint-js.yml index 03da43e..e6f4de1 100644 --- a/.github/workflows/ci-lint-js.yml +++ b/.github/workflows/ci-lint-js.yml @@ -8,15 +8,20 @@ on: type: string default: '"ubuntu-latest"' node-version: - description: "Node.js version." + description: "Node.js version. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" lint-command: - description: "Shell command that runs ESLint over the whole project. Used when changed-files is empty." + description: >- + Shell command that runs ESLint. The changed files are appended as arguments, so a custom + command must pass them on, as npm run does. Leave empty for 'npm run lint:js'. type: string - default: "npm run lint:js" + default: "" changed-files: - description: "Newline-separated list of JS files to lint. Empty = run lint-command over the whole project." + description: >- + Newline-separated changed files (ci-detect-changes' js-files). Only JS files that still + exist are linted, and a list with nothing to lint (deleted files, Jest snapshots) skips + ESLint. No list, or a change to package.json or package-lock.json, lints the whole project. type: string default: "" validate-package-json: @@ -42,10 +47,31 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -57,14 +83,25 @@ jobs: if: ${{ inputs.validate-package-json }} run: npm run lint:package-json --if-present + # ESLint errors become PR annotations through setup-node's problem matcher. - name: Run ESLint shell: bash env: CHANGED_FILES: ${{ inputs.changed-files }} - LINT_COMMAND: ${{ inputs.lint-command }} + LINT_COMMAND: ${{ inputs.lint-command || 'npm run lint:js' }} run: | - if [ -n "$CHANGED_FILES" ]; then - printf '%s\n' "$CHANGED_FILES" | xargs -d '\n' --no-run-if-empty npx --no-install eslint -- - else - bash -eo pipefail -c "$LINT_COMMAND" + set -eo pipefail + # Lint only the changed JS files that still exist; "./" keeps a name like -x.js a path. + # No list, or a package.json/lockfile change, lints the whole project. + scripts=$(printf '%s\n' "$CHANGED_FILES" | grep -E '\.[cm]?[jt]sx?$' \ + | while IFS= read -r file; do [ ! -f "$file" ] || printf './%s\n' "$file"; done || true) + others=$(printf '%s\n' "$CHANGED_FILES" | grep -vE '^$|\.[cm]?[jt]sx?$|\.snap$' || true) + files=() + if [ -n "$CHANGED_FILES" ] && [ -z "$others" ]; then + if [ -z "$scripts" ]; then + echo "::notice::Nothing to lint: the changed JS files were deleted or are Jest snapshots." + exit 0 + fi + mapfile -t files <<< "$scripts" fi + bash -eo pipefail -c "$LINT_COMMAND \"\$@\"" _ "${files[@]}" diff --git a/.github/workflows/ci-lint-php.yml b/.github/workflows/ci-lint-php.yml index 4722c33..5866e12 100644 --- a/.github/workflows/ci-lint-php.yml +++ b/.github/workflows/ci-lint-php.yml @@ -8,33 +8,35 @@ on: type: string default: '"ubuntu-latest"' php-version: - description: "PHP version." + description: "PHP version. Leave empty for 8.3." type: string - default: "8.3" + default: "" enable-phpstan: description: "Run PHPStan after PHPCS." type: boolean default: false phpcs-command: - description: "PHPCS binary path or custom command." + description: "PHPCS binary path or custom command. Leave empty for 'vendor/bin/phpcs'." type: string - default: "vendor/bin/phpcs" + default: "" changed-files: description: "Newline-separated changed files (ci-detect-changes' php-files). PHP files that still exist are linted file by file; if the list is empty or holds anything else (composer.json, composer.lock, PHPStan config), PHPCS lints the whole project. PHPStan always runs whole-project — it analyses cross-file dependencies." type: string default: "" phpstan-command: - description: "Shell command that runs PHPStan." + description: "Shell command that runs PHPStan. Leave empty for 'vendor/bin/phpstan analyse'." type: string - default: "vendor/bin/phpstan analyse" + default: "" phpstan-level: description: "Override PHPStan level. Empty uses the level from phpstan.neon.dist." type: string default: "" composer-flags: - description: "Flags passed to `composer install`. Default includes `--no-scripts` for supply-chain hygiene." + description: >- + Flags passed to `composer install`. Leave empty for '--prefer-dist --optimize-autoloader + --no-progress --no-interaction --no-scripts'; `--no-scripts` is there for supply-chain hygiene. type: string - default: "--prefer-dist --optimize-autoloader --no-progress --no-interaction --no-scripts" + default: "" validate-composer: description: "Run `composer validate` before installing. Disable for packages without composer.lock." type: boolean @@ -61,7 +63,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: - php-version: ${{ inputs.php-version }} + php-version: ${{ inputs.php-version || '8.3' }} tools: composer, cs2pr coverage: none # Raises the GitHub API rate limit when composer install resolves a VCS repo. @@ -88,7 +90,7 @@ jobs: - name: Install Composer dependencies shell: bash env: - COMPOSER_FLAGS: ${{ inputs.composer-flags }} + COMPOSER_FLAGS: ${{ inputs.composer-flags || '--prefer-dist --optimize-autoloader --no-progress --no-interaction --no-scripts' }} run: | set -euo pipefail read -r -a composer_args <<< "$COMPOSER_FLAGS" || true @@ -98,7 +100,7 @@ jobs: shell: bash env: CHANGED_FILES: ${{ inputs.changed-files }} - PHPCS_COMMAND: ${{ inputs.phpcs-command }} + PHPCS_COMMAND: ${{ inputs.phpcs-command || 'vendor/bin/phpcs' }} run: | set -euo pipefail # phpcs-command is a command and runs as shell; the report flags are appended as "$@" so @@ -125,7 +127,7 @@ jobs: if: ${{ inputs.enable-phpstan }} shell: bash env: - PHPSTAN_COMMAND: ${{ inputs.phpstan-command }} + PHPSTAN_COMMAND: ${{ inputs.phpstan-command || 'vendor/bin/phpstan analyse' }} PHPSTAN_LEVEL: ${{ inputs.phpstan-level }} run: | set -eo pipefail diff --git a/.github/workflows/ci-test-a11y.yml b/.github/workflows/ci-test-a11y.yml index 77c1e73..5962586 100644 --- a/.github/workflows/ci-test-a11y.yml +++ b/.github/workflows/ci-test-a11y.yml @@ -8,17 +8,17 @@ on: type: string default: '"ubuntu-latest"' node-version: - description: "Node.js version to install." + description: "Node.js version. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" build-command: - description: "Shell command that produces the production build pa11y will test against." + description: "Shell command that produces the production build pa11y will test against. Leave empty for 'npm run build:prod'." type: string - default: "npm run build:prod" + default: "" test-command: - description: "Shell command that runs pa11y-ci." + description: "Shell command that runs pa11y-ci. Leave empty for 'npm run test:a11y'." type: string - default: "npm run test:a11y" + default: "" working-dir: description: "Working directory for monorepos." type: string @@ -38,10 +38,31 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -52,7 +73,7 @@ jobs: - name: Build production assets shell: bash env: - BUILD_COMMAND: ${{ inputs.build-command }} + BUILD_COMMAND: ${{ inputs.build-command || 'npm run build:prod' }} run: bash -eo pipefail -c "$BUILD_COMMAND" - name: Start wp-env @@ -61,7 +82,7 @@ jobs: - name: Run pa11y-ci shell: bash env: - TEST_COMMAND: ${{ inputs.test-command }} + TEST_COMMAND: ${{ inputs.test-command || 'npm run test:a11y' }} run: bash -eo pipefail -c "$TEST_COMMAND" - name: Stop wp-env diff --git a/.github/workflows/ci-test-build-artifact.yml b/.github/workflows/ci-test-build-artifact.yml index dcfa52f..62d0324 100644 --- a/.github/workflows/ci-test-build-artifact.yml +++ b/.github/workflows/ci-test-build-artifact.yml @@ -16,13 +16,13 @@ on: type: string required: true node-version: - description: "Node.js version used to run @wordpress/env." + description: "Node.js version used to run @wordpress/env. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" php-version: - description: "PHP version wp-env boots with." + description: "PHP version wp-env boots with. Leave empty for 8.3." type: string - default: "8.3" + default: "" wp-version: description: "WordPress version wp-env boots with. 'latest' uses the newest stable release." type: string @@ -61,10 +61,31 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} - name: Download build artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -78,7 +99,7 @@ jobs: shell: bash env: WP_VERSION: ${{ inputs.wp-version }} - PHP_VERSION: ${{ inputs.php-version }} + PHP_VERSION: ${{ inputs.php-version || '8.3' }} INSTALL_PATH: ${{ inputs.install-path }} SLUG: ${{ inputs.slug }} run: | diff --git a/.github/workflows/ci-test-js.yml b/.github/workflows/ci-test-js.yml index 9e4bc97..9a00f8f 100644 --- a/.github/workflows/ci-test-js.yml +++ b/.github/workflows/ci-test-js.yml @@ -8,16 +8,16 @@ on: type: string default: '"ubuntu-latest"' node-version: - description: "Node.js version." + description: "Node.js version. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" test-command: description: >- - Shell command that runs Jest. When enable-cache is true, --cacheDirectory= is - appended as "$@", so a custom command must forward its arguments through to Jest — the - default does, via npm's `--` separator. + Shell command that runs Jest. Leave empty for 'npm run test:js -- --ci'. When + enable-cache is true, --cacheDirectory= is appended as "$@", so a custom command + must forward its arguments through to Jest — the default does, via npm's `--` separator. type: string - default: "npm run test:js -- --ci" + default: "" enable-cache: description: "Cache Jest transform/result data across runs." type: boolean @@ -41,10 +41,31 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -71,7 +92,7 @@ jobs: - name: Run Jest shell: bash env: - TEST_COMMAND: ${{ inputs.test-command }} + TEST_COMMAND: ${{ inputs.test-command || 'npm run test:js -- --ci' }} # Jest reads cacheDirectory from its config or --cacheDirectory only; there is no # environment variable for it, so the flag has to be appended to the command. JEST_CACHE_DIR: ${{ steps.jest-cache.outputs.dir }} diff --git a/.github/workflows/ci-test-php.yml b/.github/workflows/ci-test-php.yml index 900e41f..ed0161c 100644 --- a/.github/workflows/ci-test-php.yml +++ b/.github/workflows/ci-test-php.yml @@ -12,9 +12,9 @@ on: type: string default: "8.3" node-version: - description: "Node.js version (used only when use-wp-env is true)." + description: "Node.js version, used only when use-wp-env is true. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." type: string - default: "22" + default: "" wp-version: description: >- WordPress core version for wp-env, e.g. '6.8' (resolved as WordPress/WordPress#). @@ -30,9 +30,9 @@ on: type: string default: "" composer-flags: - description: "Flags passed to composer install." + description: "Flags passed to composer install. Leave empty for '--no-interaction --prefer-dist --no-progress --no-scripts'." type: string - default: "--no-interaction --prefer-dist --no-progress --no-scripts" + default: "" working-dir: description: "Working directory for monorepos." type: string @@ -67,11 +67,33 @@ jobs: # Raises the GitHub API rate limit when composer install resolves a VCS repo. github-token: ${{ github.token }} + # Node version: the input, else .nvmrc (working-dir, then repo root), else 22. + - name: Resolve Node.js version + id: node + if: ${{ inputs.use-wp-env }} + shell: bash + env: + NODE_VERSION: ${{ inputs.node-version }} + WORKING_DIR: ${{ inputs.working-dir }} + run: | + set -euo pipefail + # setup-node reads the file path from the repo root. + if [ -n "$NODE_VERSION" ]; then + echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" + elif [ -s .nvmrc ]; then + echo "file=$WORKING_DIR/.nvmrc" >> "$GITHUB_OUTPUT" + elif [ -s "$GITHUB_WORKSPACE/.nvmrc" ]; then + echo "file=.nvmrc" >> "$GITHUB_OUTPUT" + else + echo "version=22" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node.js if: ${{ inputs.use-wp-env }} uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: - node-version: ${{ inputs.node-version }} + node-version: ${{ steps.node.outputs.version }} + node-version-file: ${{ steps.node.outputs.file }} cache: npm # setup-node is a JS action: without this it looks for the lockfile at the repo root. cache-dependency-path: ${{ inputs.working-dir != '.' && format('{0}/package-lock.json', inputs.working-dir) || '' }} @@ -79,7 +101,7 @@ jobs: - name: Install Composer dependencies shell: bash env: - COMPOSER_FLAGS: ${{ inputs.composer-flags }} + COMPOSER_FLAGS: ${{ inputs.composer-flags || '--no-interaction --prefer-dist --no-progress --no-scripts' }} run: | set -euo pipefail read -r -a composer_args <<< "$COMPOSER_FLAGS" || true diff --git a/.github/workflows/wp-ci.yml b/.github/workflows/wp-ci.yml index 417ba89..43e2601 100644 --- a/.github/workflows/wp-ci.yml +++ b/.github/workflows/wp-ci.yml @@ -77,6 +77,62 @@ on: where CI generates the build output. type: string default: "" + node-version: + description: "Node.js version for every job that runs project code. Leave empty to read .nvmrc from working-dir, then the repo root, falling back to 22." + type: string + default: "" + php-version: + description: "PHP version for lint-php, build and build-artifact; test-php uses php-versions. Leave empty for each workflow's default." + type: string + default: "" + composer-flags: + description: "Flags for composer install in lint-php and test-php. Leave empty for each workflow's default." + type: string + default: "" + build-composer-flags: + description: "Flags for composer install in build, when install-composer-deps is true. Leave empty for ci-build.yml's default, which includes --no-dev." + type: string + default: "" + install-composer-deps: + description: "Install Composer dependencies before the build." + type: boolean + default: false + build-command: + description: "Shell command that produces the production build, for build and a11y. Leave empty for each workflow's default." + type: string + default: "" + lint-css-command: + description: "Shell command that runs Stylelint; changed stylesheets are appended as arguments. Leave empty for ci-lint-css.yml's default." + type: string + default: "" + lint-js-command: + description: "Shell command that runs ESLint; changed JS files are appended as arguments. Leave empty for ci-lint-js.yml's default." + type: string + default: "" + phpcs-command: + description: "PHPCS binary path or custom command. Leave empty for ci-lint-php.yml's default." + type: string + default: "" + phpstan-command: + description: "Shell command that runs PHPStan when enable-phpstan is true. Leave empty for ci-lint-php.yml's default." + type: string + default: "" + test-js-command: + description: "Shell command that runs Jest. With test-js-cache, it must forward its arguments to Jest (see ci-test-js.yml). Leave empty for ci-test-js.yml's default." + type: string + default: "" + test-js-cache: + description: "Cache Jest data across runs in test-js." + type: boolean + default: true + test-php-command: + description: "Shell command that runs PHPUnit. Leave empty for ci-test-php.yml's default." + type: string + default: "" + a11y-command: + description: "Shell command that runs pa11y-ci. Leave empty for ci-test-a11y.yml's default." + type: string + default: "" # project-type and working-dir are in the group so a monorepo caller invoking this workflow # once per unit does not have one call cancel its siblings. Uses the RAW input because @@ -184,6 +240,8 @@ jobs: with: runs-on: ${{ inputs.runs-on }} changed-files: ${{ needs.detect-changes.outputs.css-files }} + node-version: ${{ inputs.node-version }} + lint-command: ${{ inputs.lint-css-command }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} lint-js: @@ -197,6 +255,8 @@ jobs: with: runs-on: ${{ inputs.runs-on }} changed-files: ${{ needs.detect-changes.outputs.js-files }} + node-version: ${{ inputs.node-version }} + lint-command: ${{ inputs.lint-js-command }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} lint-php: @@ -211,6 +271,10 @@ jobs: runs-on: ${{ inputs.runs-on }} changed-files: ${{ needs.detect-changes.outputs.php-files }} enable-phpstan: ${{ inputs.enable-phpstan }} + php-version: ${{ inputs.php-version }} + composer-flags: ${{ inputs.composer-flags }} + phpcs-command: ${{ inputs.phpcs-command }} + phpstan-command: ${{ inputs.phpstan-command }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} test-js: @@ -223,6 +287,9 @@ jobs: uses: ./.github/workflows/ci-test-js.yml with: runs-on: ${{ inputs.runs-on }} + node-version: ${{ inputs.node-version }} + test-command: ${{ inputs.test-js-command }} + enable-cache: ${{ inputs.test-js-cache }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} test-php: @@ -247,6 +314,9 @@ jobs: php-version: ${{ matrix.php }} wp-version: ${{ matrix.wp }} use-wp-env: ${{ inputs.use-wp-env }} + node-version: ${{ inputs.node-version }} + test-command: ${{ inputs.test-php-command }} + composer-flags: ${{ inputs.composer-flags }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} build: @@ -264,6 +334,11 @@ jobs: artifact-name: ${{ needs.validate-inputs.outputs.artifact-name }} artifact-path: ${{ inputs.build-artifact-path }} retention-days: 1 + node-version: ${{ inputs.node-version }} + php-version: ${{ inputs.php-version }} + install-composer-deps: ${{ inputs.install-composer-deps }} + composer-flags: ${{ inputs.build-composer-flags }} + build-command: ${{ inputs.build-command }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} # Opt-in: runs only when build-artifact-path points at an installable dir, since the default @@ -282,6 +357,8 @@ jobs: artifact-name: ${{ needs.validate-inputs.outputs.artifact-name }} slug: ${{ needs.validate-inputs.outputs.artifact-slug }} install-path: ${{ needs.validate-inputs.outputs.install-path }} + node-version: ${{ inputs.node-version }} + php-version: ${{ inputs.php-version }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} # Opt-in: a11y is slow and needs pa11y config, so it stays off unless run-a11y is set @@ -297,6 +374,9 @@ jobs: uses: ./.github/workflows/ci-test-a11y.yml with: runs-on: ${{ inputs.runs-on }} + node-version: ${{ inputs.node-version }} + build-command: ${{ inputs.build-command }} + test-command: ${{ inputs.a11y-command }} working-dir: ${{ needs.validate-inputs.outputs.working-dir }} # Opt-in via gated-paths: fails a PR that commits build output, which is only correct where diff --git a/AGENTS.md b/AGENTS.md index fbd85ef..26a55c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,8 @@ jobs: - 2-space indent, `kebab-case` input names, a `description:` on every input, a `name:` on every job and step. `bin/check-workflows.sh` enforces all four. - Third-party actions pinned to a commit SHA with a trailing `# vX.Y.Z` comment. - Every job in a reusable workflow runs on `runs-on: ${{ fromJSON(inputs.runs-on) }}`, from a `runs-on` input that defaults to `'"ubuntu-latest"'`, and `wp-ci.yml` and `wp-cd.yml` forward it to every leaf. A reusable workflow's jobs run in the caller's repo: a public repo cannot use rtCamp's self-hosted runners, and rtCamp policy puts private repos on them, so the caller chooses (`'["self-hosted"]'` for private repos). This repo's own CI (`ci-self-check.yml`) runs on `ubuntu-latest` because the repo is public. `bin/check-workflows.sh` enforces all three. On self-hosted, pass plain `["self-hosted"]`: every pool it reached in testing was linux-x64, and the pinned binaries this repo downloads are x86_64. +- `wp-ci.yml` forwards leaf inputs, and GitHub passes an empty string through instead of falling back to the called workflow's default. So a forwarded string input defaults to `""` in `wp-ci.yml`, and the leaf turns `""` into its own default, written once, in the leaf. Booleans repeat the leaf's default. +- Leaves that run project code take Node from `node-version`, else `.nvmrc` in `working-dir`, else the repo root's `.nvmrc`, else 22. `ci-detect-changes.yml` and `version-monitor.yml` run wp-tooling, not project code, so they keep a fixed version. - Caller values reach a `run:` body through step `env:`, never through `${{ }}` interpolation. `bin/check-workflows.sh` enforces this — interpolation is still fine in `env:`, `with:` and `if:`, which are not shell. Two shapes: **Value inputs** (`composer-flags`, `phpstan-level`, `changed-files`) become argv, so shell metacharacters stay literal: diff --git a/README.md b/README.md index a5ac712..bac20f6 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ runner policy) and pass: `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. + ## Workflows Each workflow declares its inputs, secrets and outputs in its own `workflow_call` block, and GitHub From 3aca30a6bd9d794c24543e334b38fbf335a59e04 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 6 Oct 2026 10:53:36 +0530 Subject: [PATCH 2/2] chore(ci): drop redundant trailing-slash strip in cd-built-branch --- .github/workflows/cd-built-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-built-branch.yml b/.github/workflows/cd-built-branch.yml index f25e24e..0441233 100644 --- a/.github/workflows/cd-built-branch.yml +++ b/.github/workflows/cd-built-branch.yml @@ -232,7 +232,7 @@ jobs: node_version="$(jq -r --arg d "$INPUT_NODE_VERSION" '.["node-version"] // $d' <<<"$item")" node_version_file="" if [ -z "$node_version" ]; then - if [ -s "$dir/.nvmrc" ]; then node_version_file="${dir%/}/.nvmrc" + if [ -s "$dir/.nvmrc" ]; then node_version_file="$dir/.nvmrc" elif [ -s .nvmrc ]; then node_version_file=".nvmrc" else node_version="22" fi