diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index 950486c..38cdfa3 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -6,26 +6,28 @@ Workflows are defined to be reusable and modular.
Main CI pipeline used to validate code. Based on file changes it calls the following reusable workflows:
-| Reusable Workflow | What |
-| --------------------------------------- | ----------------------------------------- |
-| `reusable-phpcs.yml` | PHPCS linting |
-| `reusable-phpstan.yml` | PHPStan static analysis |
-| `reusable-phpunit.yml` | PHPUnit tests |
-| `reusable-lint-css-js.yml` | ESlint, Stylelint, Prettier, tsc linting |
-| `reusable-jest.yml` | Jest tests |
-| `reusable-e2e.yml` | Playwright end-to-end tests |
-| `reusable-build.yml` | Creates a build zip (used by playground) |
-| `reusable-wp-playground-pr-preview.yml` | PR preview environment with wp-playground |
-
-### `copilot-setup-steps.yml`
+| Reusable Workflow | What |
+| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
+| [`reusable-phpcs.yml`](.reusable-phpcs.yml)
[`reusable-phpcs-public.yml`](.reusable-phpcs-public.yml) | PHPCS linting |
+| [`reusable-phpstan.yml`](.reusable-phpstan.yml)
[`reusable-phpstan-public.yml`](.reusable-phpstan-public.yml) | PHPStan static analysis |
+| [`reusable-phpunit.yml`](.reusable-phpunit.yml)
[`reusable-phpunit-public.yml`](.reusable-phpunit-public.yml) | PHPUnit tests |
+| [`reusable-lint-css-js.yml`](.reusable-lint-css-js.yml)
[`reusable-lint-css-js-public.yml`](.reusable-lint-css-js-public.yml) | ESlint, Stylelint, Prettier, tsc linting |
+| [`reusable-jest.yml`](.reusable-jest.yml)
[`reusable-jest-public.yml`](.reusable-jest-public.yml) | Jest tests |
+| [`reusable-e2e.yml`](.reusable-e2e.yml)
[`reusable-e2e-public.yml`](.reusable-e2e-public.yml) | Playwright end-to-end tests |
+| [`reusable-build.yml`](.reusable-build.yml)
[`reusable-build-public.yml`](.reusable-build-public.yml) | Creates a build zip (used by playground) |
+| [`reusable-wp-playground-pr-preview.yml`](.reusable-wp-playground-pr-preview.yml)
[`reusable-wp-playground-pr-preview-public.yml`](.reusable-wp-playground-pr-preview-public.yml) | PR preview environment with wp-playground |
+
+Reusable workflows have a `-public` variant which is used for public GitHub runners. The non-public variants are used for rtCamp's private runners. Ensure that `ci.yml` points to the correct workflows you need, and delete the others.
+
+### [`copilot-setup-steps.yml`](copilot-setup-steps.yml)
Sets up dev environment for GitHub Copilot coding agent.
-### `pr-title.yml`
+### [`pr-title.yml`](pr-title.yml)
Triggers on PRs. Validates [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) format, required for release-please automation.
-### `release.yml`
+### [`release.yml`](release.yml)
Triggers on push to `main`. Uses [release-please](https://github.com/googleapis/release-please) to automate releases based on conventional commits.
@@ -38,9 +40,9 @@ When a release is created, it builds the plugin via `reusable-build.yml` and upl
### Secrets
-| Secret | Required By | Notes |
-| --------------- | ------------------------------------------- | ---------------------------------------------------- |
-| `CODECOV_TOKEN` | `reusable-phpunit.yml`, `reusable-jest.yml` | Optional — coverage uploads fail silently without it |
+| Secret | Required By | Notes |
+| --------------- | -------------------------------------------------------------- | ---------------------------------------------------- |
+| `CODECOV_TOKEN` | `reusable-phpunit-public.yml`
`reusable-jest-public.yml` | Optional — coverage uploads fail silently without it |
### PR Previews
@@ -72,7 +74,3 @@ act workflow_dispatch \
-s GITHUB_TOKEN=your_github_token_here \
-P ubuntu-24.04=catthehacker/ubuntu:act-latest
```
-
-## Private Runners
-
-@todo
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5663840..9fe83f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,12 +15,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
-permissions: {}
+permissions:
+ contents: read
jobs:
detect:
name: Detect Changes
- runs-on: ubuntu-24.04
+ # Use `ubuntu-24.04` on public repositories.
+ runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
permissions:
contents: read
@@ -38,8 +40,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
+ # Comment this out on private repositories to allow sharing of credentials with subsequent jobs.
+ # with:
+ # persist-credentials: false
- name: Detect file changes
uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0.0
@@ -50,17 +53,24 @@ jobs:
- '**.php'
- 'composer.*'
phpcs:
- - '.phpcs.xml.dist'
+ - '.github/workflows/ci.yml'
- '.github/workflows/reusable-phpcs.yml'
+ - '.phpcs.xml.dist'
phpstan:
- - 'phpstan.neon.dist'
+ - '.github/workflows/ci.yml'
- '.github/workflows/reusable-phpstan.yml'
+ - 'phpstan.neon.dist'
phpunit:
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/reusable-phpunit.yml'
- 'tests/**/*.php'
- 'phpunit.xml.dist'
- - '.github/workflows/reusable-phpunit.yml'
- 'package*.json'
+ - '.wp-env.test.json'
js:
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/reusable-lint-css-js.yml'
+ - '.nvmrc'
- '**.cjs'
- '**.js'
- '**.jsx'
@@ -70,17 +80,22 @@ jobs:
- '**.ts'
- '**.tsx'
- 'package*.json'
- - '.eslintrc*'
- 'tsconfig*.json'
css:
- '**.css'
- '**.scss'
+ - '.editorconfig'
+ - '.browserslistrc'
- '.stylelintignore'
- - '.stylelintrc*'
+ - '.stylelint.config.js'
e2e:
- - 'tests/e2e/**/*'
+ - '.github/workflows/ci.yml'
- '.github/workflows/reusable-e2e.yml'
+ - 'tests/e2e/**/*'
+ - 'playwright.config.ts'
jest:
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/reusable-jest.yml'
- 'tests/js/**/*'
- 'src/**/__tests__/**/*'
- 'src/**/*.test.ts'
@@ -88,14 +103,15 @@ jobs:
- 'src/**/*.spec.ts'
- 'src/**/*.spec.tsx'
- 'jest.config.js'
- - '.github/workflows/reusable-jest.yml'
- 'package*.json'
+ - '.wp-env.test.json'
phpcs:
name: PHPCS
needs: detect
if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpcs == 'true'
uses: ./.github/workflows/reusable-phpcs.yml
+ # uses: ./.github/workflows/reusable-phpcs-public.yml
permissions:
contents: read
with:
@@ -106,6 +122,7 @@ jobs:
needs: detect
if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpstan == 'true'
uses: ./.github/workflows/reusable-phpstan.yml
+ # uses: ./.github/workflows/reusable-phpstan-public.yml
permissions:
contents: read
with:
@@ -116,6 +133,7 @@ jobs:
needs: detect
if: needs.detect.outputs.css == 'true' || needs.detect.outputs.js == 'true'
uses: ./.github/workflows/reusable-lint-css-js.yml
+ # uses: ./.github/workflows/reusable-lint-css-js-public.yml
permissions:
contents: read
@@ -124,6 +142,7 @@ jobs:
needs: detect
if: needs.detect.outputs.js == 'true' || needs.detect.outputs.jest == 'true'
uses: ./.github/workflows/reusable-jest.yml
+ # uses: ./.github/workflows/reusable-jest-public.yml
permissions:
contents: read
with:
@@ -131,13 +150,16 @@ jobs:
phpunit:
name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wp }})
- needs: detect
+ needs: [detect, build-plugin-zip]
if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpunit == 'true'
uses: ./.github/workflows/reusable-phpunit.yml
+ # uses: ./.github/workflows/reusable-phpunit-public.yml
permissions:
contents: read
strategy:
fail-fast: false
+ # Uncomment this on private runners, which can't run multiple wp-env instances in parallel.
+ max-parallel: 1
matrix:
php: ['8.4', '8.3', '8.2']
wp: ['latest']
@@ -153,9 +175,11 @@ jobs:
e2e:
name: E2E Tests
- needs: detect
+ # needs: detect
+ needs: phpunit
if: needs.detect.outputs.php == 'true' || needs.detect.outputs.e2e == 'true' || needs.detect.outputs.js == 'true' || needs.detect.outputs.css == 'true'
uses: ./.github/workflows/reusable-e2e.yml
+ # uses: ./.github/workflows/reusable-e2e-public.yml
permissions:
contents: read
with:
@@ -166,6 +190,7 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/reusable-build.yml
+ # uses: ./.github/workflows/reusable-build-public.yml
with:
php-version: '8.2'
artifact-name: plugin-skeleton-d-pr${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
@@ -176,6 +201,7 @@ jobs:
needs: build-plugin-zip
if: github.event_name == 'pull_request'
uses: ./.github/workflows/reusable-wp-playground-pr-preview.yml
+ # uses: ./.github/workflows/reusable-wp-playground-pr-preview-public.yml
permissions:
actions: read
contents: write
diff --git a/.github/workflows/reusable-build-public.yml b/.github/workflows/reusable-build-public.yml
new file mode 100644
index 0000000..012c136
--- /dev/null
+++ b/.github/workflows/reusable-build-public.yml
@@ -0,0 +1,66 @@
+name: Reusable Build
+
+on:
+ workflow_call:
+ inputs:
+ php-version:
+ required: true
+ type: string
+ artifact-name:
+ required: true
+ type: string
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
+ with:
+ php-version: ${{ inputs.php-version }}
+ coverage: none
+
+ - name: Install Composer dependencies
+ uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3.2.0
+ with:
+ composer-options: '--no-dev --optimize-autoloader'
+
+ - name: Setup Node.js
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Build plugin for release
+ run: npm run build:prod
+
+ - name: Start the Docker testing environment
+ uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: npm run wp-env start
+
+ - name: Generate Translation files
+ run: npm run i18n:make-pot
+
+ - name: Create plugin zip
+ run: npm run plugin-zip
+
+ - name: Upload plugin zip artifact
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: ${{ inputs.artifact-name }}
+ path: plugin-skeleton-d.zip
+ if-no-files-found: error
diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml
index 012c136..5bf8041 100644
--- a/.github/workflows/reusable-build.yml
+++ b/.github/workflows/reusable-build.yml
@@ -12,15 +12,46 @@ on:
jobs:
build:
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
+ options: --user root -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/wp-env:/tmp/wp-env
permissions:
contents: read
+
+ env:
+ WP_ENV_PORT: 8889
+ WP_ENV_HOME: /tmp/wp-env
+
+ timeout-minutes: 30
+
steps:
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get install -y git curl sudo unzip jq
+
+ - name: setup Docker
+ run: |
+ curl -fsSL https://get.docker.com -o get-docker.sh
+ sh get-docker.sh
+
+ - name: Configure Docker group permissions
+ run: |
+ useradd -m wpuser
+ SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
+ groupmod -g $SOCK_GID -o docker
+ usermod -aG docker wpuser
+ mkdir -p /tmp/wp-env
+ chown -R wpuser:wpuser /tmp/wp-env
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Set up PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
@@ -45,15 +76,39 @@ jobs:
- name: Build plugin for release
run: npm run build:prod
+ - name: Fix wp-env host mapping
+ run: |
+ # Get the host path for the workspace by inspecting the container's own mounts
+ HOST_WORKSPACE_PATH=$(docker inspect $(hostname) --format '{{ range .Mounts }}{{ if eq .Destination "/__w" }}{{ .Source }}{{ end }}{{ end }}')
+
+ # Construct the full host path for the current plugin directory
+ RELATIVE_PATH=$(pwd | sed 's|^/__w||')
+ PLUGIN_HOST_PATH="${HOST_WORKSPACE_PATH}${RELATIVE_PATH}"
+
+ echo "Host Workspace: $HOST_WORKSPACE_PATH"
+ echo "Plugin Host Path: $PLUGIN_HOST_PATH"
+
+ # Update .wp-env.json (default env)
+ jq --arg path "$PLUGIN_HOST_PATH" \
+ 'del(.plugins) | .mappings["wp-content/plugins/plugin-skeleton-d"] = $path' \
+ .wp-env.json > .wp-env.json.tmp && mv .wp-env.json.tmp .wp-env.json
+
+ cat .wp-env.json
+
+ - name: Fix workspace permissions
+ run: |
+ chown -R wpuser:wpuser .
+ sudo -u wpuser git config --global --add safe.directory '/tmp/wp-env/*'
+
- name: Start the Docker testing environment
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
- command: npm run wp-env start
+ command: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" "WP_ENV_PORT=$WP_ENV_PORT" npm run wp-env start -- --update
- name: Generate Translation files
- run: npm run i18n:make-pot
+ run: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run i18n:make-pot
- name: Create plugin zip
run: npm run plugin-zip
@@ -64,3 +119,7 @@ jobs:
name: ${{ inputs.artifact-name }}
path: plugin-skeleton-d.zip
if-no-files-found: error
+
+ - name: Stop the Docker testing environment
+ if: always()
+ run: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run wp-env destroy -- --force
diff --git a/.github/workflows/reusable-e2e-public.yml b/.github/workflows/reusable-e2e-public.yml
new file mode 100644
index 0000000..1f4637c
--- /dev/null
+++ b/.github/workflows/reusable-e2e-public.yml
@@ -0,0 +1,64 @@
+name: Run E2E Tests
+
+on:
+ workflow_call:
+ inputs:
+ php-version:
+ description: 'PHP version to use'
+ required: true
+ type: string
+
+permissions: {}
+
+jobs:
+ e2e:
+ name: Playwright E2E Tests
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
+ with:
+ php-version: ${{ inputs.php-version }}
+ coverage: none
+
+ - name: Install Composer dependencies
+ uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3.2.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Build development assets
+ run: npm run build:dev
+
+ - name: Start the Docker testing environment
+ uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: npm run wp-env:test start
+
+ - name: Run E2E tests
+ run: npm run test:e2e
+
+ - name: Upload E2E test results as artifact
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ if: always()
+ with:
+ name: playwright-report
+ path: tests/_output
+ retention-days: 2
+ if-no-files-found: ignore
diff --git a/.github/workflows/reusable-e2e.yml b/.github/workflows/reusable-e2e.yml
index e5dce7d..546a8d5 100644
--- a/.github/workflows/reusable-e2e.yml
+++ b/.github/workflows/reusable-e2e.yml
@@ -8,20 +8,55 @@ on:
required: true
type: string
-permissions: {}
+permissions:
+ contents: read
jobs:
e2e:
name: Playwright E2E Tests
- runs-on: ubuntu-24.04
- timeout-minutes: 20
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
+ # We use --add-host to get the host IP, and we will use socat to forward
+ # localhost:8889 to the host so Playwright and WordPress agree on "localhost".
+ options: --user root --add-host=host.docker.internal:host-gateway -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/wp-env:/tmp/wp-env -v /tmp/playwright-cache:/tmp/playwright-cache
+
+ timeout-minutes: 30
+
+ env:
+ WP_ENV_PORT: 8889
+ WP_ENV_HOME: /tmp/wp-env
+ PLAYWRIGHT_BROWSERS_PATH: /tmp/playwright-cache
steps:
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get install -y git curl sudo unzip jq socat
+
+ - name: setup Docker
+ run: |
+ curl -fsSL https://get.docker.com -o get-docker.sh
+ sh get-docker.sh
+
+ - name: Configure Docker group permissions
+ run: |
+ useradd -m wpuser
+ SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
+ groupmod -g $SOCK_GID -o docker
+ usermod -aG docker wpuser
+ mkdir -p /tmp/wp-env
+ mkdir -p /tmp/playwright-cache
+ chown -R wpuser:wpuser /tmp/wp-env
+ chown -R wpuser:wpuser /tmp/playwright-cache
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Set up PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
@@ -44,15 +79,50 @@ jobs:
- name: Build development assets
run: npm run build:dev
+ - name: Fix wp-env host mapping
+ run: |
+ # Get the host path for the workspace by inspecting the container's own mounts
+ HOST_WORKSPACE_PATH=$(docker inspect $(hostname) --format '{{ range .Mounts }}{{ if eq .Destination "/__w" }}{{ .Source }}{{ end }}{{ end }}')
+
+ # Construct the full host path for the current plugin directory
+ RELATIVE_PATH=$(pwd | sed 's|^/__w||')
+ PLUGIN_HOST_PATH="${HOST_WORKSPACE_PATH}${RELATIVE_PATH}"
+
+ echo "Host Workspace: $HOST_WORKSPACE_PATH"
+ echo "Plugin Host Path: $PLUGIN_HOST_PATH"
+
+ # Update .wp-env.test.json
+ jq --arg path "$PLUGIN_HOST_PATH" \
+ 'del(.plugins) | .mappings["wp-content/plugins/plugin-skeleton-d"] = $path' \
+ .wp-env.test.json > .wp-env.test.json.tmp && mv .wp-env.test.json.tmp .wp-env.test.json
+
+ cat .wp-env.test.json
+
+ - name: Fix workspace permissions
+ run: |
+ chown -R wpuser:wpuser .
+ sudo -u wpuser git config --global --add safe.directory '/tmp/wp-env/*'
+
- name: Start the Docker testing environment
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
- timeout_minutes: 10
+ timeout_minutes: 15
max_attempts: 3
- command: WP_ENV_PORT=8889 WP_ENV_TESTS_PORT=8891 npm run wp-env:test start
+ command: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" "WP_ENV_PORT=$WP_ENV_PORT" npm run wp-env:test start -- --update
+
+ - name: Install Playwright dependencies
+ run: |
+ npx playwright install-deps
+ sudo -u wpuser env "PATH=$PATH" "PLAYWRIGHT_BROWSERS_PATH=$PLAYWRIGHT_BROWSERS_PATH" npx playwright install
- name: Run E2E tests
- run: npm run test:e2e
+ run: |
+ # Forward localhost traffic from the CI container to the Host machine
+ # where the WordPress Docker container is actually listening.
+ # This ensures Playwright and WordPress both agree the domain is "localhost:8889".
+ socat TCP4-LISTEN:8889,fork,bind=127.0.0.1 TCP4:host.docker.internal:8889 &
+
+ sudo -u wpuser env "PATH=$PATH" "PLAYWRIGHT_BROWSERS_PATH=$PLAYWRIGHT_BROWSERS_PATH" npm run test:e2e
- name: Upload E2E test results as artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
@@ -62,3 +132,7 @@ jobs:
path: tests/_output
retention-days: 2
if-no-files-found: ignore
+
+ - name: Stop the Docker testing environment
+ if: always()
+ run: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run wp-env:test destroy -- --force
diff --git a/.github/workflows/reusable-jest-public.yml b/.github/workflows/reusable-jest-public.yml
new file mode 100644
index 0000000..719dc08
--- /dev/null
+++ b/.github/workflows/reusable-jest-public.yml
@@ -0,0 +1,55 @@
+name: Run Jest Tests
+
+on:
+ workflow_call:
+ inputs:
+ coverage:
+ description: 'Enable code coverage'
+ required: false
+ type: boolean
+ default: true
+
+permissions: {}
+
+jobs:
+ jest:
+ name: Jest Unit Tests${{ inputs.coverage && ' with coverage' || '' }}
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Run Jest tests${{ inputs.coverage && ' with coverage' || '' }}
+ run: npm run test:js${{ inputs.coverage && ':coverage' || '' }}
+
+ - name: Upload code coverage report to Codecov
+ if: ${{ inputs.coverage }}
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: tests/_output/js-coverage/lcov.info
+ flags: jest
+ fail_ci_if_error: false
+
+ - name: Upload HTML coverage report as artifact
+ if: ${{ inputs.coverage }}
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: jest-code-coverage
+ path: tests/_output/js-coverage/lcov-report
+ retention-days: 7
+ if-no-files-found: ignore
diff --git a/.github/workflows/reusable-jest.yml b/.github/workflows/reusable-jest.yml
index 719dc08..e514a9c 100644
--- a/.github/workflows/reusable-jest.yml
+++ b/.github/workflows/reusable-jest.yml
@@ -9,12 +9,15 @@ on:
type: boolean
default: true
-permissions: {}
+permissions:
+ contents: read
jobs:
jest:
name: Jest Unit Tests${{ inputs.coverage && ' with coverage' || '' }}
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: node:22
timeout-minutes: 10
steps:
@@ -22,7 +25,9 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
diff --git a/.github/workflows/reusable-lint-css-js-public.yml b/.github/workflows/reusable-lint-css-js-public.yml
new file mode 100644
index 0000000..c855250
--- /dev/null
+++ b/.github/workflows/reusable-lint-css-js-public.yml
@@ -0,0 +1,54 @@
+name: Run JS Lint
+
+on:
+ workflow_call:
+
+permissions: {}
+
+jobs:
+ lint-js:
+ name: JS Lint & TypeScript
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Run ESLint
+ id: eslint
+ continue-on-error: true
+ run: npm run lint:js
+
+ - name: Run TypeScript check
+ id: typescript
+ continue-on-error: true
+ run: npm run lint:js:types
+
+ - name: Run Stylelint
+ id: stylelint
+ continue-on-error: true
+ run: npm run lint:css
+
+ - name: Run Prettier check
+ id: prettier
+ continue-on-error: true
+ run: npm run format -- --check
+
+ - name: Check for errors
+ if: ${{ steps.eslint.outcome == 'failure' || steps.typescript.outcome == 'failure' || steps.prettier.outcome == 'failure' || steps.stylelint.outcome == 'failure' }}
+ run: |
+ echo "One or more checks failed. See logs above for details."
+ exit 1
diff --git a/.github/workflows/reusable-lint-css-js.yml b/.github/workflows/reusable-lint-css-js.yml
index c855250..c64166b 100644
--- a/.github/workflows/reusable-lint-css-js.yml
+++ b/.github/workflows/reusable-lint-css-js.yml
@@ -3,12 +3,15 @@ name: Run JS Lint
on:
workflow_call:
-permissions: {}
+permissions:
+ contents: read
jobs:
lint-js:
name: JS Lint & TypeScript
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: node:22
timeout-minutes: 20
steps:
@@ -16,7 +19,9 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
diff --git a/.github/workflows/reusable-phpcs-public.yml b/.github/workflows/reusable-phpcs-public.yml
new file mode 100644
index 0000000..dbfec74
--- /dev/null
+++ b/.github/workflows/reusable-phpcs-public.yml
@@ -0,0 +1,53 @@
+name: Run PHPCS
+
+on:
+ workflow_call:
+ inputs:
+ php-version:
+ description: 'PHP version to use'
+ required: true
+ type: string
+
+permissions: {}
+
+jobs:
+ phpcs:
+ name: PHPCS Coding Standards
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
+ with:
+ php-version: ${{ inputs.php-version }}
+ coverage: none
+ tools: cs2pr
+
+ # This date is used to ensure that the PHPCS cache is cleared at least once every week.
+ - name: "Get last Monday's date"
+ id: get-date
+ run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT"
+
+ - name: Cache PHPCS scan cache
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
+ with:
+ path: tests/_output/phpcs-cache.json
+ key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-phpcs-cache-${{ hashFiles('**/composer.json', '.phpcs.xml.dist') }}
+
+ - name: Install Composer dependencies
+ uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3.2.0
+
+ - name: Run PHPCS
+ id: phpcs
+ run: composer lint -- --report-full --report-checkstyle=./tests/_output/phpcs-report.xml
+
+ - name: Show PHPCS results in PR
+ if: ${{ always() && steps.phpcs.outcome == 'failure' }}
+ run: cs2pr ./tests/_output/phpcs-report.xml
diff --git a/.github/workflows/reusable-phpcs.yml b/.github/workflows/reusable-phpcs.yml
index dbfec74..0ec4c4a 100644
--- a/.github/workflows/reusable-phpcs.yml
+++ b/.github/workflows/reusable-phpcs.yml
@@ -8,20 +8,30 @@ on:
required: true
type: string
-permissions: {}
+permissions:
+ contents: read
jobs:
phpcs:
name: PHPCS Coding Standards
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
timeout-minutes: 20
steps:
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get install -y git curl sudo unzip
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Set up PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
diff --git a/.github/workflows/reusable-phpstan-public.yml b/.github/workflows/reusable-phpstan-public.yml
new file mode 100644
index 0000000..d815852
--- /dev/null
+++ b/.github/workflows/reusable-phpstan-public.yml
@@ -0,0 +1,56 @@
+name: Run PHPStan
+
+on:
+ workflow_call:
+ inputs:
+ php-version:
+ description: 'PHP version to use'
+ required: true
+ type: string
+
+permissions: {}
+
+jobs:
+ phpstan:
+ name: PHPStan Static Analysis
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
+ with:
+ php-version: ${{ inputs.php-version }}
+ coverage: none
+ tools: cs2pr
+
+ # This date is used to ensure that the PHPStan cache is cleared at least once every week.
+ - name: "Get last Monday's date"
+ id: get-date
+ run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT"
+
+ - name: Cache PHPStan scan cache
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
+ with:
+ path: tests/_output
+ key: phpstan-result-cache-${{ runner.os }}-date-${{ steps.get-date.outputs.date }}
+
+ - name: Install Composer dependencies
+ uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3.2.0
+
+ - name: Run PHPStan
+ id: phpstan
+ run: composer run-script phpstan -- --memory-limit=1G --error-format=checkstyle | cs2pr
+
+ - name: Save result cache
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
+ if: ${{ !cancelled() }}
+ with:
+ path: tests/_output
+ key: phpstan-result-cache-${{ runner.os }}-date-${{ steps.get-date.outputs.date }}
diff --git a/.github/workflows/reusable-phpstan.yml b/.github/workflows/reusable-phpstan.yml
index d815852..a19fb14 100644
--- a/.github/workflows/reusable-phpstan.yml
+++ b/.github/workflows/reusable-phpstan.yml
@@ -8,20 +8,30 @@ on:
required: true
type: string
-permissions: {}
+permissions:
+ contents: read
jobs:
phpstan:
name: PHPStan Static Analysis
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
timeout-minutes: 20
steps:
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get install -y git curl sudo unzip
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Set up PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
diff --git a/.github/workflows/reusable-phpunit-public.yml b/.github/workflows/reusable-phpunit-public.yml
new file mode 100644
index 0000000..2a990ba
--- /dev/null
+++ b/.github/workflows/reusable-phpunit-public.yml
@@ -0,0 +1,105 @@
+name: Run PHPUnit Tests
+
+on:
+ workflow_call:
+ inputs:
+ php-version:
+ description: 'PHP version to use'
+ required: true
+ type: string
+ wp-version:
+ description: 'WordPress version (latest, trunk, or X.Y)'
+ required: false
+ type: string
+ default: 'latest'
+ coverage:
+ description: 'Enable code coverage'
+ required: false
+ type: boolean
+ default: false
+ multisite:
+ description: 'Run multisite tests'
+ required: false
+ type: boolean
+ default: false
+
+permissions: {}
+
+jobs:
+ phpunit:
+ name: PHPUnit (PHP ${{ inputs.php-version }}, WP ${{ inputs.wp-version }})${{ inputs.coverage && ' with coverage' || '' }}${{ inputs.multisite && ' multisite' || '' }}
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ env:
+ WP_ENV_PHP_VERSION: ${{ inputs.php-version }}
+ WP_ENV_CORE: ${{ inputs.wp-version == 'trunk' && 'WordPress/WordPress' || format('https://wordpress.org/wordpress-{0}.zip', inputs.wp-version) }}
+
+ steps:
+ - name: Configure environment variables
+ run: |
+ echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV"
+ echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV"
+
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+ persist-credentials: false
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
+ with:
+ php-version: ${{ inputs.php-version }}
+ coverage: ${{ inputs.coverage && 'xdebug' || 'none' }}
+
+ - name: Install Composer dependencies
+ uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3.2.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Build development assets
+ run: npm run build:dev
+
+ - name: Start the Docker testing environment
+ uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: |
+ if [ "${{ inputs.coverage }}" == "true" ]; then
+ npm run wp-env:test start -- --xdebug=coverage
+ else
+ npm run wp-env:test start
+ fi
+
+ - name: Log versions
+ run: |
+ npm run wp-env:test -- run cli php -- -v
+ npm run wp-env:test -- run cli wp core version
+
+ - name: Run PHPUnit tests${{ inputs.coverage && ' with coverage report' || '' }}
+ run: npm run test:php ${{ !inputs.coverage && '-- --no-coverage' || '' }}
+
+ - name: Upload code coverage report
+ if: ${{ inputs.coverage }}
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: tests/_output/php-coverage.xml
+ flags: unit
+ fail_ci_if_error: false
+
+ - name: Upload HTML coverage report as artifact
+ if: ${{ inputs.coverage }}
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: wp-code-coverage-${{ inputs.php-version }}-${{ inputs.wp-version }}
+ path: tests/_output/html
+ overwrite: true
diff --git a/.github/workflows/reusable-phpunit.yml b/.github/workflows/reusable-phpunit.yml
index 2a990ba..1977809 100644
--- a/.github/workflows/reusable-phpunit.yml
+++ b/.github/workflows/reusable-phpunit.yml
@@ -23,18 +23,44 @@ on:
type: boolean
default: false
-permissions: {}
+permissions:
+ contents: read
jobs:
phpunit:
name: PHPUnit (PHP ${{ inputs.php-version }}, WP ${{ inputs.wp-version }})${{ inputs.coverage && ' with coverage' || '' }}${{ inputs.multisite && ' multisite' || '' }}
- runs-on: ubuntu-24.04
- timeout-minutes: 20
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
+ options: --user root -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/wp-env:/tmp/wp-env
+
+ timeout-minutes: 30
+
env:
WP_ENV_PHP_VERSION: ${{ inputs.php-version }}
WP_ENV_CORE: ${{ inputs.wp-version == 'trunk' && 'WordPress/WordPress' || format('https://wordpress.org/wordpress-{0}.zip', inputs.wp-version) }}
+ WP_ENV_PORT: 8889
+ WP_ENV_HOME: /tmp/wp-env
steps:
+ - name: Install system dependencies
+ run: |
+ apt update
+ apt-get install -y git curl sudo unzip jq
+
+ - name: setup Docker
+ run: |
+ curl -fsSL https://get.docker.com -o get-docker.sh
+ sh get-docker.sh
+
+ - name: Configure Docker group permissions
+ run: |
+ useradd -m wpuser -s /bin/bash
+ SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
+ groupmod -g $SOCK_GID -o docker
+ usermod -aG docker wpuser
+ chown -R wpuser:wpuser /tmp/wp-env
+
- name: Configure environment variables
run: |
echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV"
@@ -44,7 +70,9 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- persist-credentials: false
+
+ - name: Configure Git for Safe Directory
+ run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
- name: Set up PHP
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
@@ -67,25 +95,51 @@ jobs:
- name: Build development assets
run: npm run build:dev
+ - name: Fix wp-env host mapping
+ run: |
+ # Get the host path for the workspace by inspecting the container's own mounts
+ HOST_WORKSPACE_PATH=$(docker inspect $(hostname) --format '{{ range .Mounts }}{{ if eq .Destination "/__w" }}{{ .Source }}{{ end }}{{ end }}')
+
+ # Construct the full host path for the current plugin directory
+ # We strip the internal /__w prefix and append it to the host workspace path
+ RELATIVE_PATH=$(pwd | sed 's|^/__w||')
+ PLUGIN_HOST_PATH="${HOST_WORKSPACE_PATH}${RELATIVE_PATH}"
+
+ echo "Host Workspace: $HOST_WORKSPACE_PATH"
+ echo "Plugin Host Path: $PLUGIN_HOST_PATH"
+
+ # 1. Remove the "plugins" field to prevent broken automatic mounts
+ # 2. Add the host mapping
+ jq --arg path "$PLUGIN_HOST_PATH" \
+ 'del(.plugins) | .mappings["wp-content/plugins/plugin-skeleton-d"] = $path' \
+ .wp-env.test.json > .wp-env.test.json.tmp && mv .wp-env.test.json.tmp .wp-env.test.json
+
+ cat .wp-env.test.json
+
+ - name: Fix workspace permissions
+ run: |
+ chown -R wpuser:wpuser .
+ sudo -u wpuser git config --global --add safe.directory '/tmp/wp-env/*'
+
- name: Start the Docker testing environment
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
- timeout_minutes: 10
+ timeout_minutes: 15
max_attempts: 3
command: |
if [ "${{ inputs.coverage }}" == "true" ]; then
- npm run wp-env:test start -- --xdebug=coverage
+ sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" "WP_ENV_PORT=$WP_ENV_PORT" npm run wp-env:test start -- --xdebug=coverage --update --debug
else
- npm run wp-env:test start
+ sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" "WP_ENV_PORT=$WP_ENV_PORT" npm run wp-env:test start -- --update
fi
- name: Log versions
run: |
- npm run wp-env:test -- run cli php -- -v
- npm run wp-env:test -- run cli wp core version
+ sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run wp-env:test -- run cli php -- -v
+ sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run wp-env:test -- run cli wp core version
- name: Run PHPUnit tests${{ inputs.coverage && ' with coverage report' || '' }}
- run: npm run test:php ${{ !inputs.coverage && '-- --no-coverage' || '' }}
+ run: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" "WP_ENV_PORT=$WP_ENV_PORT" "WP_ENV_TESTS_PORT=$WP_ENV_TESTS_PORT" npm run test:php ${{ !inputs.coverage && '-- --no-coverage' || '' }}
- name: Upload code coverage report
if: ${{ inputs.coverage }}
@@ -103,3 +157,7 @@ jobs:
name: wp-code-coverage-${{ inputs.php-version }}-${{ inputs.wp-version }}
path: tests/_output/html
overwrite: true
+
+ - name: Stop the Docker testing environment
+ if: always()
+ run: sudo -u wpuser env "PATH=$PATH" "WP_ENV_HOME=$WP_ENV_HOME" npm run wp-env:test destroy -- --force
diff --git a/.github/workflows/reusable-wp-playground-pr-preview-public.yml b/.github/workflows/reusable-wp-playground-pr-preview-public.yml
new file mode 100644
index 0000000..45e87bf
--- /dev/null
+++ b/.github/workflows/reusable-wp-playground-pr-preview-public.yml
@@ -0,0 +1,123 @@
+name: Run WordPress Playground PR Preview
+
+on:
+ workflow_call:
+ inputs:
+ run-id:
+ description: 'The workflow run ID to download artifacts from'
+ required: true
+ type: string
+ artifact-prefix:
+ description: 'Prefix for the artifact name (e.g., plugin-skeleton-d-pr)'
+ required: false
+ type: string
+ default: 'plugin-skeleton-d-pr'
+ artifact-filename:
+ description: 'Filename of the zip inside the artifact'
+ required: false
+ type: string
+ default: 'plugin-skeleton-d.zip'
+ artifacts-to-keep:
+ description: 'Number of artifacts to keep per PR'
+ required: false
+ type: string
+ default: '2'
+
+permissions: {}
+
+jobs:
+ playground-preview:
+ name: Post Playground Preview
+ runs-on: ubuntu-24.04
+ permissions:
+ actions: read
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Extract PR metadata and artifact info
+ id: pr-metadata
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ const prefix = '${{ inputs.artifact-prefix }}';
+
+ const run = await github.rest.actions.getWorkflowRun({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: '${{ inputs.run-id }}',
+ });
+
+ const pulls = run.data.pull_requests;
+ if (!pulls || pulls.length === 0) {
+ core.setFailed('No pull request found in workflow_run');
+ return;
+ }
+ const prNumber = pulls[0].number;
+ const commitSha = run.data.head_sha;
+
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: '${{ inputs.run-id }}',
+ });
+
+ const artifact = artifacts.data.artifacts.find(a =>
+ a.name.startsWith(prefix)
+ );
+
+ if (!artifact) {
+ core.setFailed(`Could not find artifact with prefix: ${prefix}`);
+ return;
+ }
+
+ core.setOutput('pr-number', prNumber.toString());
+ core.setOutput('commit-sha', commitSha);
+ core.setOutput('artifact-name', artifact.name);
+
+ - name: Expose built artifact on public URL
+ id: expose
+ uses: WordPress/action-wp-playground-pr-preview/.github/actions/expose-artifact-on-public-url@c8607529dac8d2bf9a1e8493865fc97cd1c3c87b # v2
+ with:
+ artifact-name: ${{ steps.pr-metadata.outputs.artifact-name }}
+ artifact-filename: ${{ inputs.artifact-filename }}
+ pr-number: ${{ steps.pr-metadata.outputs.pr-number }}
+ commit-sha: ${{ steps.pr-metadata.outputs.commit-sha }}
+ artifact-source-run-id: ${{ inputs.run-id }}
+ artifacts-to-keep: ${{ inputs.artifacts-to-keep }}
+
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: blueprint.json
+ sparse-checkout-cone-mode: false
+
+ - name: Generate Playground blueprint JSON
+ id: blueprint
+ run: |
+ node - <<'NODE' >> "$GITHUB_OUTPUT"
+ const fs = require('fs');
+ const url = process.env.ARTIFACT_URL;
+ if (!url) {
+ throw new Error('ARTIFACT_URL is required');
+ }
+
+ // Load the base blueprint and update the plugin URL
+ const blueprint = JSON.parse(fs.readFileSync('blueprint.json', 'utf8'));
+ const installStep = blueprint.steps.find(s => s.step === 'installPlugin');
+ if (installStep) {
+ installStep.pluginData.url = url;
+ }
+
+ console.log(`blueprint=${JSON.stringify(blueprint)}`);
+ NODE
+ env:
+ ARTIFACT_URL: ${{ steps.expose.outputs.artifact-url }}
+
+ - name: Post Playground preview button
+ uses: WordPress/action-wp-playground-pr-preview@c8607529dac8d2bf9a1e8493865fc97cd1c3c87b # v2
+ with:
+ mode: append-to-description
+ blueprint: ${{ steps.blueprint.outputs.blueprint }}
+ pr-number: ${{ steps.pr-metadata.outputs.pr-number }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/reusable-wp-playground-pr-preview.yml b/.github/workflows/reusable-wp-playground-pr-preview.yml
index e25a1b1..c96e93e 100644
--- a/.github/workflows/reusable-wp-playground-pr-preview.yml
+++ b/.github/workflows/reusable-wp-playground-pr-preview.yml
@@ -23,18 +23,30 @@ on:
type: string
default: '2'
-permissions: {}
+permissions:
+ contents: read
jobs:
playground-preview:
name: Post Playground Preview
- runs-on: ubuntu-24.04
+ runs-on: [public-repo]
+ container:
+ image: ubuntu:24.04
permissions:
actions: read
contents: write
pull-requests: write
steps:
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get install -y git curl jq gpg
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null
+ apt-get update
+ apt install -y gh
+
- name: Extract PR metadata and artifact info
id: pr-metadata
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
@@ -45,7 +57,7 @@ jobs:
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: ${{ inputs.run-id }},
+ run_id: '${{ inputs.run-id }}',
});
const pulls = run.data.pull_requests;
@@ -59,7 +71,7 @@ jobs:
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: ${{ inputs.run-id }},
+ run_id: '${{ inputs.run-id }}',
});
const artifact = artifacts.data.artifacts.find(a =>
@@ -95,22 +107,12 @@ jobs:
- name: Generate Playground blueprint JSON
id: blueprint
run: |
- node - <<'NODE' >> "$GITHUB_OUTPUT"
- const fs = require('fs');
- const url = process.env.ARTIFACT_URL;
- if (!url) {
- throw new Error('ARTIFACT_URL is required');
- }
-
- // Load the base blueprint and update the plugin URL
- const blueprint = JSON.parse(fs.readFileSync('blueprint.json', 'utf8'));
- const installStep = blueprint.steps.find(s => s.step === 'installPlugin');
- if (installStep) {
- installStep.pluginData.url = url;
- }
-
- console.log(`blueprint=${JSON.stringify(blueprint)}`);
- NODE
+ if [ -z "$ARTIFACT_URL" ]; then
+ echo "::error::ARTIFACT_URL is required"
+ exit 1
+ fi
+ BLUEPRINT=$(jq --arg url "$ARTIFACT_URL" '(.steps[] | select(.step == "installPlugin") | .pluginData.url) = $url' blueprint.json)
+ echo "blueprint=$(echo $BLUEPRINT | jq -c .)" >> "$GITHUB_OUTPUT"
env:
ARTIFACT_URL: ${{ steps.expose.outputs.artifact-url }}
diff --git a/.github/workflows/test-public-workflows.yml b/.github/workflows/test-public-workflows.yml
new file mode 100644
index 0000000..f6adb4e
--- /dev/null
+++ b/.github/workflows/test-public-workflows.yml
@@ -0,0 +1,181 @@
+# These are used by the repository to test the GitHub workflows for the repository.
+# Projects using the skeleton should remove this file, and updated the `ci.yml` workflow
+name: CI (Public Workflows)
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ - release/**
+ types: [opened, synchronize, reopened, ready_for_review]
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ detect:
+ name: Detect Changes
+ runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
+ permissions:
+ contents: read
+ pull-requests: read
+ outputs:
+ php: ${{ steps.filter.outputs.php }}
+ phpcs: ${{ steps.filter.outputs.phpcs }}
+ phpstan: ${{ steps.filter.outputs.phpstan }}
+ phpunit: ${{ steps.filter.outputs.phpunit }}
+ js: ${{ steps.filter.outputs.js }}
+ css: ${{ steps.filter.outputs.css }}
+ e2e: ${{ steps.filter.outputs.e2e }}
+ jest: ${{ steps.filter.outputs.jest }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Detect file changes
+ uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0.0
+ id: filter
+ with:
+ filters: |
+ php:
+ - 'composer.*'
+ phpcs:
+ - '.github/workflows/reusable-phpcs-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - '.phpcs.xml.dist'
+ phpstan:
+ - '.github/workflows/reusable-phpstan-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - 'phpstan.neon.dist'
+ phpunit:
+ - '.github/workflows/reusable-phpunit-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - 'phpunit.xml.dist'
+ - 'package*.json'
+ js:
+ - '.github/workflows/reusable-lint-css-js-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - '.eslintrc.js'
+ - '.nvmrc'
+ - '.prettierrc.js'
+ - 'package*.json'
+ - '.eslintrc*'
+ - 'tsconfig*.json'
+ css:
+ - '.stylelintignore'
+ - '.stylelint.config.js*'
+ e2e:
+ - '.github/workflows/reusable-e2e-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - 'playwright.config.ts'
+ jest:
+ - '.github/workflows/reusable-jest-public.yml'
+ - '.github/workflows/test-public-workflows.yml'
+ - 'jest.config.js'
+ - 'package*.json'
+
+ phpcs:
+ name: PHPCS
+ needs: detect
+ if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpcs == 'true'
+ uses: ./.github/workflows/reusable-phpcs-public.yml
+ permissions:
+ contents: read
+ with:
+ php-version: '8.2'
+
+ phpstan:
+ name: PHPStan
+ needs: detect
+ if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpstan == 'true'
+ uses: ./.github/workflows/reusable-phpstan-public.yml
+ permissions:
+ contents: read
+ with:
+ php-version: '8.2'
+
+ lint-css-js:
+ name: CSS/JS Lint
+ needs: detect
+ if: needs.detect.outputs.css == 'true' || needs.detect.outputs.js == 'true'
+ uses: ./.github/workflows/reusable-lint-css-js.yml
+ permissions:
+ contents: read
+
+ jest:
+ name: Jest Unit Tests
+ needs: detect
+ if: needs.detect.outputs.js == 'true' || needs.detect.outputs.jest == 'true'
+ uses: ./.github/workflows/reusable-jest.yml
+ permissions:
+ contents: read
+ with:
+ coverage: true
+
+ phpunit:
+ name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wp }})
+ needs: detect
+ if: needs.detect.outputs.php == 'true' || needs.detect.outputs.phpunit == 'true'
+ uses: ./.github/workflows/reusable-phpunit.yml
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.4']
+ wp: ['latest']
+ include:
+ - php: '8.4'
+ wp: 'latest'
+ coverage: true
+ with:
+ php-version: ${{ matrix.php }}
+ wp-version: ${{ matrix.wp }}
+ coverage: ${{ matrix.coverage == true }}
+ secrets: inherit
+
+ e2e:
+ name: E2E Tests
+ needs: detect
+ if: needs.detect.outputs.php == 'true' || needs.detect.outputs.e2e == 'true' || needs.detect.outputs.js == 'true' || needs.detect.outputs.css == 'true'
+ uses: ./.github/workflows/reusable-e2e-public.yml
+ permissions:
+ contents: read
+ with:
+ php-version: '8.2'
+
+ build-plugin-zip:
+ name: Build Plugin Zip
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-build-public.yml
+ with:
+ php-version: '8.2'
+ artifact-name: plugin-skeleton-d-pr-public${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
+ secrets: inherit
+
+ playground-preview:
+ name: Playground Preview
+ needs: build-plugin-zip
+ if: github.event_name == 'pull_request'
+ uses: ./.github/workflows/reusable-wp-playground-pr-preview-public.yml
+ permissions:
+ actions: read
+ contents: write
+ pull-requests: write
+ with:
+ run-id: ${{ github.run_id }}
+ artifact-prefix: 'plugin-skeleton-d-pr-public'
+ artifact-filename: 'plugin-skeleton-d.zip'
diff --git a/.wp-env.test.json b/.wp-env.test.json
index b8f64fd..dc3871c 100644
--- a/.wp-env.test.json
+++ b/.wp-env.test.json
@@ -8,8 +8,5 @@
"FS_METHOD": "direct",
"WP_DEBUG": true,
"WP_DEBUG_LOG": "/var/www/html/wp-content/plugins/plugin-skeleton-d/tests/_output/debug-test.log"
- },
- "mappings": {
- "wp-content/plugins/plugin-skeleton-d": "."
}
}
diff --git a/playwright.config.ts b/playwright.config.ts
index 6023d14..89cb960 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -18,10 +18,18 @@ process.env[ 'STORAGE_STATE_PATH' ] = path.join(
const baseConfig =
require( '@wordpress/scripts/config/playwright.config.js' ) as PlaywrightTestConfig; // eslint-disable-line @typescript-eslint/no-var-requires
+// Disable Playwright's automatic webServer orchestration to prevent port
+// conflicts, as the CI workflow/local scripts manually manage the wp-env lifecycle.
+const { webServer, ...baseConfigWithoutWebServer } = baseConfig;
+
const config = defineConfig( {
- ...baseConfig,
+ ...baseConfigWithoutWebServer,
testDir: './tests/e2e',
outputDir: './tests/_output/e2e',
+ use: {
+ ...baseConfigWithoutWebServer.use,
+ headless: true,
+ },
} );
export default config;