Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/scripts/fill-in-scaffold.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Fills in the template's placeholders with a plugin's identity. fill-in-scaffold.yml runs it once in a plugin
# generated from the template, and Quality rehearses it on the template with sample inputs.
set -euo pipefail

: "${PLUGIN_NAME:?}" "${SLUG:?}" "${NS:?}" "${PREFIX:?}" "${PORT:?}" "${REPO:?}"

# The slug names the entry file and the text domain, so it stays clean kebab-case.
grep -Eq '^[a-z][a-z0-9]*(-[a-z0-9]+)*$' <<< "$SLUG" || { echo "::error::plugin-slug must be kebab-case starting with a letter, with no leading, trailing or double hyphen."; exit 1; }
grep -Eq '^[A-Za-z][A-Za-z0-9]*$' <<< "$NS" || { echo "::error::php-namespace must match ^[A-Za-z][A-Za-z0-9]*$."; exit 1; }
# One segment after dws_ keeps every prefix in one plugin's family from starting another plugin's, and the
# framework's admin notices refuse a prefix over 32 characters or with a network or hidden segment.
{ grep -Eq '^dws_[a-z0-9]{1,28}$' <<< "$PREFIX" && [ "$PREFIX" != dws_network ] && [ "$PREFIX" != dws_hidden ]; } || { echo "::error::prefix must match ^dws_[a-z0-9]{1,28}$ and must not be dws_network or dws_hidden."; exit 1; }
# Both ports land in JSON numbers, where a leading zero is invalid, and the tests environment takes the next one;
# neither may be the template's own 8811 or 8812, which the leftover check below refuses.
{ grep -Eq '^[1-9][0-9]{0,4}$' <<< "$PORT" && [ "$PORT" -le 65534 ] && { [ "$PORT" -lt 8810 ] || [ "$PORT" -gt 8812 ]; }; } || { echo "::error::wp-env-port must be 1-65534 with no leading zero, and neither it nor the next port may be 8811 or 8812."; exit 1; }
# The display name lands in single-quoted PHP and JavaScript strings.
if printf '%s' "$PLUGIN_NAME" | grep -q "['\\\$/]"; then echo "::error::plugin-name must not contain \\, \$, / or '."; exit 1; fi

export SCREAMING_PREFIX="${PREFIX^^}"
export TESTS_PORT=$(( PORT + 1 ))
export REPO_NAME="${REPO#*/}"

git mv --force README.scaffold.md README.md
git rm --quiet includes/_disabled-example.php
# The template's recipe test loads the recipe's safeguard helper, which a plugin that applies the recipe declares
# itself, so only the recipe's own test stays.
git rm --quiet tests/Integration/SafeguardRecipeTest.php
git mv dws-plugin-template.php "$SLUG.php"

# GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the workflows read the slug from composer.json
# instead. The BEGIN filter drops symlinks (CLAUDE.md -> AGENTS.md), which in-place editing would turn into files.
# shellcheck disable=SC2016
git ls-files -z -- . ':!.github/workflows/' ':!.github/scripts/fill-in-scaffold.sh' | xargs -0 perl -pi -e '
BEGIN { @ARGV = grep { ! -l $_ } @ARGV }
s{ahegyes/wordpress-plugin-template}{$ENV{REPO}}g;
s/^# wordpress-plugin-template$/# $ENV{REPO_NAME}/;
s/dws-plugin-template/$ENV{SLUG}/g;
s/dws_tpl/$ENV{PREFIX}/g;
s/DWS_TPL/$ENV{SCREAMING_PREFIX}/g;
s/PluginTemplate/$ENV{NS}/g;
s/DWS Plugin Template/$ENV{PLUGIN_NAME}/g;
'

# The locks' integrity hashes and version strings belong to other packages.
# shellcheck disable=SC2016
git ls-files -z -- . ':!.github/workflows/' ':!.github/scripts/fill-in-scaffold.sh' ':!composer.lock' ':!package-lock.json' | xargs -0 perl -pi -e '
BEGIN { @ARGV = grep { ! -l $_ } @ARGV }
s/\b(881[12])\b/8811 == $1 ? $ENV{PORT} : $ENV{TESTS_PORT}/ge;
s/^(\s*\*\s*\@(?:since|version)\s+)2\.0\.0$/${1}0.1.0/;
'
# shellcheck disable=SC2016
perl -pi -e 's/^(\s*\*\s*Version:\s*)2\.0\.0$/${1}0.1.0/' "$SLUG.php"
perl -pi -e 's/^## 2\.0\.0 - unreleased$/## 0.1.0 - unreleased/' CHANGELOG.md
find changelog -type f ! -name '.gitkeep' -delete

# The sweep renamed the package, so the lock's content hash follows, or composer validate --strict fails.
composer update --lock --no-install --no-scripts --no-plugins --ignore-platform-req=php+ --no-interaction

composer validate --strict
php -l "$SLUG.php"
php -l uninstall.php
# shellcheck disable=SC2016
for swept_json in .wp-env.json .wp-env.tests.json composer.json package.json package-lock.json; do
php -r 'json_decode( (string) file_get_contents( $argv[1] ), false, 512, JSON_THROW_ON_ERROR );' "$swept_json"
done

status=0
if git grep -n -I -E 'dws-plugin-template|dws_tpl|DWS_TPL|PluginTemplate|DWS Plugin Template|wordpress-plugin-template' -- . ':!.github/scripts/fill-in-scaffold.sh'; then
echo "::error::The fill-in left the template's names above."
status=1
fi
if git grep -n -I -E '\b881[12]\b' -- . ':!.github/scripts/fill-in-scaffold.sh' ':!composer.lock' ':!package-lock.json'; then
echo "::error::The fill-in left the template's ports above."
status=1
fi
if ! git diff --quiet HEAD -- .github/workflows; then
echo "::error::The fill-in changed a workflow, which GitHub refuses to push with the job's token."
status=1
fi
exit "$status"
2 changes: 1 addition & 1 deletion .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ jobs:
audit:
name: Audit
# npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts.
uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@9f64dc77b25727130884eaf427d2bd75493557ce
with:
npm-audit-flags: '--omit=dev --audit-level=high'
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
actions: read
contents: read
security-events: write
uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@9f64dc77b25727130884eaf427d2bd75493557ce
150 changes: 31 additions & 119 deletions .github/workflows/fill-in-scaffold.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
name: Fill in scaffold

# Fork-only, one-shot: substitutes every template placeholder, renames the entry file, resets the
# version/changelog/metadata, and pushes the result back to the ref the workflow ran on.
#
# Placeholder inventory the sweep covers (each swept as a plain case-sensitive substring):
# dws-plugin-template slug + text-domain (also inside ahegyes/dws-plugin-template + the npm name)
# dws_plugin_template option-key base + settings-page slug (the trailing-underscore function
# prefix is this base plus `_`, so one substitution covers both forms)
# DWS_PLUGIN_TEMPLATE constants
# PluginTemplate namespace segment — covers DeepWebSolutions\PluginTemplate, the doubled-
# backslash JSON forms (DeepWebSolutions\\PluginTemplate\\ in composer.json),
# and the scoper prefix, since only the segment between backslashes changes
# DWS Plugin Template display name (plugin header, readme.txt, translated strings, e2e spec)
# ahegyes/wordpress-plugin-template repository-metadata URLs (composer homepage/authors, package.json
# repository/bugs, SECURITY.md advisory link, readme.txt changelog link,
# plugin header URI) — rewritten to the fork's own repository
# wordpress-plugin-template bare repo name — only as the AGENTS.md h1, so the rule is anchored to that
# heading (a blanket rule would re-match inside a fork repo name that itself
# contains the template name)
# 8811 wp-env port (.wp-env*.json, playwright.config.js, README, agent docs)
# 2.0.0 → 0.1.0 version reset (plugin header, _VERSION constant, readme Stable tag,
# @since/@version docblocks, test literals; the 1.0.0 pre-update seed in
# tests becomes 0.0.1 so the update-path test still seeds an older version)

on:
workflow_dispatch:
inputs:
Expand All @@ -31,124 +8,59 @@ on:
required: true
type: string
plugin-slug:
description: 'Kebab-case slug (e.g. "acme-orders")'
description: 'Kebab-case slug, also the text domain and the entry file name (e.g. "acme-orders")'
required: true
type: string
php-namespace:
description: 'PHP namespace segment (e.g. "AcmeOrders")'
description: 'PHP namespace segment after DeepWebSolutions\ (e.g. "AcmeOrders")'
required: true
type: string
prefix:
description: 'Prefix of the global functions, constants, options and hooks, dws_ plus one segment (e.g. "dws_ao")'
required: true
type: string
wp-env-port:
description: 'wp-env port'
required: false
description: 'Development wp-env port; the tests environment takes the next one (e.g. "8821")'
required: true
type: string
default: '8811'

# contents: write + persisted credentials are the point: the job commits the filled-in scaffold and pushes it back.
permissions:
contents: write
contents: read

jobs:
fill-in:
name: Fill in scaffold
# One-shot fork bootstrap — running it on the template would destroy the placeholders it exists to substitute.
if: github.repository != 'ahegyes/wordpress-plugin-template'
# Running it on the template would destroy the placeholders it exists to fill in.
if: ${{ !github.event.repository.is_template }}
runs-on: ubuntu-latest
env:
PLUGIN_NAME: ${{ inputs.plugin-name }}
SLUG: ${{ inputs.plugin-slug }}
NS: ${{ inputs.php-namespace }}
PORT: ${{ inputs.wp-env-port }}
REPO: ${{ github.repository }}
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Explain the template-repo guard
run: echo "Running on $REPO — the job-level guard refuses the template repo itself, where the sweep would destroy the placeholders."

- name: Validate inputs
run: |
# The slug feeds PHP constants and the entry-file rename — it must start with a letter and stay clean kebab-case.
echo "$SLUG" | grep -Eq '^[a-z][a-z0-9]*(-[a-z0-9]+)*$' || { echo "::error::plugin-slug must be kebab-case starting with a letter (no leading/trailing/double hyphen)"; exit 1; }
echo "$NS" | grep -Eq '^[A-Za-z][A-Za-z0-9]*$' || { echo "::error::php-namespace must match ^[A-Za-z][A-Za-z0-9]*$"; exit 1; }
# The port lands in JSON numeric fields, where a leading zero is invalid JSON.
{ echo "$PORT" | grep -Eq '^[1-9][0-9]{0,4}$' && [ "$PORT" -le 65535 ]; } || { echo "::error::wp-env-port must be 1-65535 with no leading zero"; exit 1; }
# The display name lands in perl replacements and in single-quoted PHP/JS string literals.
if printf '%s' "$PLUGIN_NAME" | grep -q "['\\\$/]"; then echo "::error::plugin-name must not contain \\, \$, /, or '"; exit 1; fi

# persist-credentials stays on: this job's whole purpose is pushing the filled-in scaffold back.
# The job pushes the filled-in plugin back, so the checkout keeps its credentials.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true

- name: Compute derived forms
run: |
{
echo "SNAKE=$(echo "$SLUG" | tr '-' '_')"
echo "SCREAMING=$(echo "$SLUG" | tr '-' '_' | tr '[:lower:]' '[:upper:]')"
echo "REPO_NAME=${REPO#*/}"
} >> "$GITHUB_ENV"

- name: Substitute placeholders
# The sweep skips this workflow file so the repo guard keeps its literal template name; a re-run
# after fill-in finds nothing left to substitute.
run: |
# $ENV{...} is perl, resolved inside the single-quoted program on purpose. The BEGIN filter drops
# symlinks (CLAUDE.md -> AGENTS.md): in-place editing through one would replace it with a regular file.
# shellcheck disable=SC2016
git ls-files -z | grep -zv '^.github/workflows/fill-in-scaffold.yml$' | xargs -0 perl -pi -e '
BEGIN { @ARGV = grep { ! -l $_ } @ARGV }
s{ahegyes/wordpress-plugin-template}{$ENV{REPO}}g;
s/^# wordpress-plugin-template$/# $ENV{REPO_NAME}/;
s/dws-plugin-template/$ENV{SLUG}/g;
s/dws_plugin_template/$ENV{SNAKE}/g;
s/DWS_PLUGIN_TEMPLATE/$ENV{SCREAMING}/g;
s/PluginTemplate/$ENV{NS}/g;
s/DWS Plugin Template/$ENV{PLUGIN_NAME}/g;
'

- name: Substitute the wp-env port
# CLAUDE.md is a symlink to AGENTS.md, so sweeping AGENTS.md covers both.
run: |
perl -pi -e 's/8811/$ENV{PORT}/g' .wp-env.json .wp-env.belowfloor.json playwright.config.js README.md AGENTS.md

- name: Rename the entry file
run: git mv dws-plugin-template.php "$SLUG.php"

- name: Reset the version to 0.1.0
run: |
# ${1} is a perl backreference inside the single-quoted program.
# shellcheck disable=SC2016
# Docblock lines first (all tracked text files except the lockfiles, whose 2.0.0 hits are foreign packages).
git ls-files -z | grep -zv -e '^composer.lock$' -e '^package-lock.json$' | xargs -0 perl -pi -e 'BEGIN { @ARGV = grep { ! -l $_ } @ARGV } s/^(\s*\*\s*\@(?:since|version)\s+)2\.0\.0$/${1}0.1.0/'
# Entry file: plugin header Version + the _VERSION constant line.
perl -pi -e 's/\b2\.0\.0\b/0.1.0/g' "$SLUG.php"
# readme.txt Stable tag.
perl -pi -e 's/^(Stable tag:\s*)2\.0\.0$/${1}0.1.0/' readme.txt
# Tests assert against the constant's value; the 1.0.0 pre-update seed must stay below the new version.
git ls-files -z 'tests/*' | xargs -0 perl -pi -e 's/\b2\.0\.0\b/0.1.0/g; s/\b1\.0\.0\b/0.0.1/g'

- name: Reset the changelog
run: |
perl -0pi -e 's/<!-- Start changelog -->.*<!-- End changelog -->/<!-- Start changelog -->\n\n## 0.1.0 - unreleased\n\n<!-- End changelog -->/s' CHANGELOG.md readme.txt
find changelog -type f ! -name '.gitkeep' -delete

- name: Reset readme.txt prose to placeholders
run: |
perl -0pi -e 's/== Description ==.*?== Installation ==/== Description ==\n\nTODO: Describe the plugin for the wp.org listing.\n\n== Installation ==/s' readme.txt
perl -pi -e 's/^(Tags:\s*).*$/${1}TODO/' readme.txt
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: '8.5'
coverage: none
tools: composer:v2

- name: Verify the result
run: |
composer validate --no-check-lock
php -l "$SLUG.php"
php -l uninstall.php
# The sweep rewrote these as text — prove they are still valid JSON before anything is pushed.
# shellcheck disable=SC2016
for swept_json in .wp-env.json .wp-env.belowfloor.json composer.json package.json package-lock.json; do
php -r 'json_decode( (string) file_get_contents( $argv[1] ), false, 512, JSON_THROW_ON_ERROR );' "$swept_json"
done
- name: Fill in the placeholders
env:
PLUGIN_NAME: ${{ inputs.plugin-name }}
SLUG: ${{ inputs.plugin-slug }}
NS: ${{ inputs.php-namespace }}
PREFIX: ${{ inputs.prefix }}
PORT: ${{ inputs.wp-env-port }}
REPO: ${{ github.repository }}
run: .github/scripts/fill-in-scaffold.sh

# Pushes back to the ref the workflow ran on — run it on trunk for the standard fork flow.
- name: Commit and push
env:
SLUG: ${{ inputs.plugin-slug }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
Expand Down
65 changes: 59 additions & 6 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,49 @@ permissions:
jobs:
lint-php:
name: PHP Lint
uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@9f64dc77b25727130884eaf427d2bd75493557ce
with:
php-version: '8.5'
scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]'

lint-scripts-styles:
name: Scripts and styles lint
uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@9f64dc77b25727130884eaf427d2bd75493557ce
with:
scripts: '["lint:scripts", "lint:styles", "lint:types"]'

block-json:
name: block.json schema
uses: ahegyes/wordpress-configs/.github/workflows/reusable-block-json-check.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
uses: ahegyes/wordpress-configs/.github/workflows/reusable-block-json-check.yml@9f64dc77b25727130884eaf427d2bd75493557ce

slug:
name: Plugin slug
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
slug: ${{ steps.slug.outputs.slug }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares.
- name: Read the slug from composer.json
id: slug
run: |
slug=$(jq -r '.extra["text-domain"] // empty' composer.json)
if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'."
exit 1
fi
echo "slug=$slug" >> "$GITHUB_OUTPUT"

php-syntax:
name: PHP syntax
uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-syntax-check.yml@a6a70735ba7a94628011b9856bf99159c490f7b5
needs: slug
uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-syntax-check.yml@9f64dc77b25727130884eaf427d2bd75493557ce
with:
php-versions: '["7.4"]'
paths: '["dws-plugin-template.php", "uninstall.php", "includes"]'
paths: ${{ format('["{0}.php", "uninstall.php", "includes"]', needs.slug.outputs.slug) }}

changelog:
name: Changelog fragments
Expand All @@ -40,10 +62,41 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: '8.5'
coverage: none
tools: composer:v2
- uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0
- run: composer changelog:validate

scaffold:
name: Scaffold rehearsal
# A plugin generated from the template has filled in its placeholders already, so only the template rehearses the fill-in.
if: github.event.repository.is_template
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: '8.5'
coverage: none
tools: composer:v2
- name: Fill in the scaffold
env:
PLUGIN_NAME: Acme Orders
SLUG: acme-orders
NS: AcmeOrders
PREFIX: dws_ao
PORT: '8821'
REPO: acme/acme-orders
run: .github/scripts/fill-in-scaffold.sh
- name: Install and scope the filled-in plugin
run: composer packages-install
- name: Lint and unit-test the filled-in plugin
run: |
composer lint:php
composer test:unit
Loading
Loading