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;