├── LICENSE ├── README.md ├── bin ├── acceptance-tests ├── build-vendor ├── integration-tests ├── tests-destroy ├── tests-start └── tests-stop ├── composer.json ├── config ├── nginx.conf ├── php-config.ini └── wp-config.php ├── phpstan ├── extension.neon └── stubs.php └── tests └── docker-compose.yml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 John Blackbourn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plugin Infrastructure 2 | 3 | Reusable infrastructure relating to testing, building, and deploying my WordPress plugins (see the "Used by" section below). 4 | 5 | Provided without support, warranty, guarantee, backwards compatibility, fitness for purpose, resilience, safety, sanity, beauty, or support for any plugin that isn't one of mine. 6 | 7 | ## Used by 8 | 9 | * [Extended CPTs](https://github.com/johnbillion/extended-cpts) 10 | * [Query Monitor](https://github.com/johnbillion/query-monitor) 11 | * [User Switching](https://github.com/johnbillion/user-switching) 12 | * [WP Crontrol](https://github.com/johnbillion/wp-crontrol) 13 | 14 | ## Features 15 | 16 | * Acceptance testing 17 | * Integration testing 18 | * Coding standards testing 19 | * Static analysis 20 | * Workflow file linting 21 | * Deployment to wordpress.org 22 | * Build provenance attestation 23 | * SLSA v1.0 Build level 3 facilitation 24 | 25 | ## Overview 26 | 27 | Plugins that use this library all use a similar setup in their workflows: 28 | 29 | ### Acceptance testing 30 | 31 | * Push to a main branch or pull request, `acceptance-tests.yml` fires 32 | * Constructs a matrix of supported PHP and WordPress versions 33 | * Uses `reusable-acceptance-tests.yml` 34 | * Installs PHP and WordPress 35 | * Runs the build 36 | * Runs acceptance testing with wp-browser 37 | 38 | ### Integration testing 39 | 40 | * Push to a main branch or pull request, `integration-tests.yml` fires 41 | * Constructs a matrix of supported PHP and WordPress versions 42 | * Uses `reusable-integration-tests.yml` 43 | * Installs PHP and WordPress 44 | * Runs the build 45 | * Runs integration testing with wp-browser, once for: 46 | * Single site 47 | * Multisite 48 | 49 | ### Coding standards testing 50 | 51 | * Push to a main branch or pull request, `coding-standards.yml` fires 52 | * Uses `reusable-coding-standards.yml` 53 | * Installs PHP 54 | * Checks coding standards with PHPCS 55 | 56 | ### Static analysis 57 | 58 | * Push to a main branch or pull request, `static-analysis.yml` fires 59 | * Constructs a matrix of supported PHP versions 60 | * Uses `reusable-static-analysis.yml` 61 | * Installs PHP 62 | * Runs static analysis with PHPStan 63 | 64 | ### Workflow file linting 65 | 66 | * Push to a main branch or pull request, `lint-workflows.yml` fires 67 | * Uses `reusable-workflow-lint.yml` 68 | * Lints all GitHub Actions workflow files for correctness and security using: 69 | * ActionLint 70 | * Octoscan 71 | * Zizmor 72 | * Poutine 73 | * OpenSSF Scorecard 74 | * Uploads results to GitHub Code Scanning 75 | 76 | ### Deployment to wordpress.org 77 | 78 | * Push to the `release` branch, `build.yml` fires 79 | * Uses `reusable-build.yml` 80 | * Runs the build 81 | * Reads version from `package.json` 82 | * Commits built files 83 | * Pushes to `release-$VERSION` 84 | * Tags the new version and pushes 85 | * Creates a draft release 86 | * Publish the release, `deploy-tag.yml` fires 87 | * Uses `reusable-deploy-tag.yml` 88 | * Creates a changelog entry from the release notes 89 | * Uses `10up/action-wordpress-plugin-deploy` 90 | * Deploys the new version to wordpress.org 91 | * Generates a zip file 92 | * Uses `johnbillion/action-wordpress-plugin-attestation` 93 | * Fetches the zip from wordpress.org 94 | * Generates a build provenance attestation if the zip contents matches the build 95 | * Closes the completed milestone for the release 96 | * Creates the next major, minor, and patch release milestones 97 | 98 | ## Licence 99 | 100 | MIT 101 | -------------------------------------------------------------------------------- /bin/acceptance-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Args 8 | PLUGIN=${PWD##*/} 9 | 10 | # Environment variables 11 | export COMPOSE_PROJECT_NAME=${PLUGIN} 12 | 13 | echo "ℹ️ Starting up..." 14 | 15 | # Get the dynamic port number for a service: 16 | get_service_port() { 17 | docker compose port "$1" "$2" | awk -F: '{print $2}' 18 | } 19 | 20 | # Prep: 21 | WP_PORT=$(get_service_port server 80) 22 | CHROME_PORT=$(get_service_port chrome 4444) 23 | DATABASE_PORT=$(get_service_port database 3306) 24 | WP_URL="http://host.docker.internal:${WP_PORT}" 25 | 26 | wp() { 27 | docker compose run --quiet-pull --rm wpcli --url="${WP_URL}" "$@" 28 | } 29 | 30 | # Wait for the database server: 31 | while ! docker compose exec -T database /bin/bash -c 'mysqladmin ping --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" --silent' | grep 'mysqld is alive' >/dev/null; do 32 | echo 'ℹ️ Waiting for database server ping...' 33 | sleep 3 34 | done 35 | while ! docker compose exec -T database /bin/bash -c 'mysql --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" --execute="SHOW DATABASES;"' | grep 'information_schema' >/dev/null; do 36 | echo 'ℹ️ Waiting for database server query...' 37 | sleep 3 38 | done 39 | 40 | # Wait for Selenium: 41 | while ! curl -sSL "http://localhost:${CHROME_PORT}/wd/hub/status" 2>&1 | grep '"ready": true' >/dev/null; do 42 | echo 'ℹ️ Waiting for Selenium...' 43 | sleep 3 44 | done 45 | 46 | # Reset or install the test database: 47 | echo 'ℹ️ Installing database...' 48 | wp db reset --yes 49 | 50 | # Install WordPress: 51 | echo 'ℹ️ Installing WordPress...' 52 | wp core install \ 53 | --title="${PLUGIN}" \ 54 | --admin_user="admin" \ 55 | --admin_password="admin" \ 56 | --admin_email="admin@example.com" \ 57 | --skip-email \ 58 | --exec="mysqli_report( MYSQLI_REPORT_OFF );" 59 | echo "ℹ️ Home URL: $WP_URL" 60 | 61 | # Set a predictable permalink structure: 62 | wp rewrite structure '/%postname%/' 63 | 64 | # Activate the plugin under test: 65 | wp plugin activate ${PLUGIN} 66 | 67 | CODECEPT_ARGS="" 68 | 69 | for flag; do 70 | # If the flag starts with `--cli=`, then we want to run the command: 71 | if [[ $flag == --cli=* ]]; then 72 | # Remove the prefix: 73 | cli_command="${flag#--cli=}" 74 | # Run the command: 75 | wp $cli_command 76 | # Otherwise, we want to pass the flag to Codeception: 77 | else 78 | CODECEPT_ARGS="$CODECEPT_ARGS $flag" 79 | fi 80 | done 81 | 82 | # Run the acceptance tests: 83 | echo 'ℹ️ Running tests...' 84 | TEST_SITE_WEBDRIVER_PORT=$CHROME_PORT \ 85 | TEST_SITE_DATABASE_PORT=$DATABASE_PORT \ 86 | TEST_SITE_WP_URL=$WP_URL \ 87 | ./vendor/bin/codecept run acceptance "${CODECEPT_ARGS}" 88 | 89 | echo '✅ Acceptance tests complete.' 90 | -------------------------------------------------------------------------------- /bin/build-vendor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Install non-dev Composer dependencies: 8 | echo 'ℹ️ Removing composer/installers...' 9 | composer remove composer/installers 10 | echo 'ℹ️ Removing dev dependencies...' 11 | composer update --no-dev 12 | echo 'ℹ️ Dumping non-dev autoloader...' 13 | composer dump-autoload --no-dev 14 | 15 | # Wrap the call to `setClassMapAuthoritative` in a `method_exists` check: 16 | echo 'ℹ️ Wrapping setClassMapAuthoritative in method_exists check...' 17 | sed -i.bak 's/^ \$loader->setClassMapAuthoritative(true);/ if (method_exists(\$loader,"setClassMapAuthoritative")){\n \$loader->setClassMapAuthoritative(true);\n }/' "${PWD}/vendor/composer/autoload_real.php" 18 | echo 'ℹ️ Removing backup file...' 19 | rm "${PWD}/vendor/composer/autoload_real.php.bak" 20 | 21 | # Confirm that the change was successful: 22 | if ! grep -q 'method_exists(\$loader,"setClassMapAuthoritative")' "${PWD}/vendor/composer/autoload_real.php" >/dev/null; then 23 | echo 'ℹ️ setClassMapAuthoritative replacement failed!' 24 | exit 1 25 | fi 26 | 27 | # Remove autoloading for `\Composer\InstalledVersions`: 28 | echo 'ℹ️ Removing autoloading for \Composer\InstalledVersions...' 29 | sed -i.bak '/Composer\\\\InstalledVersions/d' "${PWD}/vendor/composer/autoload_static.php" 30 | echo 'ℹ️ Removing backup file...' 31 | rm "${PWD}/vendor/composer/autoload_static.php.bak" 32 | 33 | # Confirm that the change was successful: 34 | if grep -q 'Composer\\\\InstalledVersions' "${PWD}/vendor/composer/autoload_static.php" >/dev/null; then 35 | echo 'ℹ️ Composer\InstalledVersions deletion failed!' 36 | exit 1 37 | fi 38 | 39 | # Remove files not needed for deployment: 40 | echo 'ℹ️ Removing files not needed for deployment...' 41 | rm -f "${PWD}/vendor/composer/autoload_classmap.php" 42 | rm -f "${PWD}/vendor/composer/autoload_files.php" 43 | rm -f "${PWD}/vendor/composer/autoload_namespaces.php" 44 | rm -f "${PWD}/vendor/composer/autoload_psr4.php" 45 | rm -f "${PWD}/vendor/composer/installed.json" 46 | rm -f "${PWD}/vendor/composer/InstalledVersions.php" 47 | 48 | echo '✅ Vendor build complete.' 49 | -------------------------------------------------------------------------------- /bin/integration-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Args 8 | PLUGIN=${PWD##*/} 9 | CODECEPT_ARGS=$1 10 | 11 | # Environment variables 12 | export COMPOSE_PROJECT_NAME=${PLUGIN} 13 | 14 | echo 'ℹ️ Starting up...' 15 | 16 | # Wait for the database server: 17 | while ! docker compose exec -T database /bin/bash -c 'mysqladmin ping --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" --silent' | grep 'mysqld is alive' >/dev/null; do 18 | echo 'ℹ️ Waiting for database server ping...' 19 | sleep 3 20 | done 21 | while ! docker compose exec -T database /bin/bash -c 'mysql --user="${MYSQL_USER}" --password="${MYSQL_PASSWORD}" --execute="SHOW DATABASES;"' | grep 'information_schema' >/dev/null; do 22 | echo 'ℹ️ Waiting for database server query...' 23 | sleep 3 24 | done 25 | 26 | # Run the integration tests: 27 | echo 'ℹ️ Running tests...' 28 | 29 | # Why are these sent to /dev/null? See https://github.com/docker/compose/issues/8833 30 | docker compose exec \ 31 | --env COMPOSE_PROJECT_NAME \ 32 | -T \ 33 | --workdir "/var/www/html/wp-content/plugins/${PLUGIN}" php \ 34 | ./vendor/bin/codecept run integration --env singlesite --skip-group ms-required "${CODECEPT_ARGS}" \ 35 | < /dev/null 36 | 37 | docker compose exec \ 38 | --env COMPOSE_PROJECT_NAME \ 39 | -T \ 40 | --workdir "/var/www/html/wp-content/plugins/${PLUGIN}" php \ 41 | ./vendor/bin/codecept run integration --env multisite --skip-group ms-excluded "${CODECEPT_ARGS}" \ 42 | < /dev/null 43 | 44 | echo '✅ Integration tests complete.' 45 | -------------------------------------------------------------------------------- /bin/tests-destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Args 8 | PLUGIN=${PWD##*/} 9 | 10 | # Environment variables 11 | export COMPOSE_PROJECT_NAME=${PLUGIN} 12 | 13 | docker compose down --volumes --remove-orphans 14 | -------------------------------------------------------------------------------- /bin/tests-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Args 8 | PLUGIN=${PWD##*/} 9 | 10 | # Environment variables 11 | export COMPOSE_PROJECT_NAME=${PLUGIN} 12 | 13 | docker compose up --quiet-pull -d 14 | -------------------------------------------------------------------------------- /bin/tests-stop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e Exit immediately if a pipeline returns a non-zero status 4 | # -o pipefail Produce a failure return code if any command errors 5 | set -eo pipefail 6 | 7 | # Args 8 | PLUGIN=${PWD##*/} 9 | 10 | # Environment variables 11 | export COMPOSE_PROJECT_NAME=${PLUGIN} 12 | 13 | docker compose down 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "johnbillion/plugin-infrastructure", 3 | "description": "Reusable infrastructure relating to testing, building, and deploying my WordPress plugins", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "John Blackbourn", 9 | "homepage": "https://johnblackbourn.com/" 10 | } 11 | ], 12 | "funding": [ 13 | { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/johnbillion" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.4" 20 | }, 21 | "require-dev": { 22 | }, 23 | "bin": [ 24 | "bin/acceptance-tests", 25 | "bin/integration-tests", 26 | "bin/tests-destroy", 27 | "bin/tests-start", 28 | "bin/tests-stop" 29 | ], 30 | "config": { 31 | "sort-packages": true 32 | }, 33 | "scripts": { 34 | "test": [ 35 | "composer validate --strict --no-check-lock" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | index index.php index.html; 3 | 4 | listen 80 default_server; 5 | listen [::]:80 default_server; 6 | 7 | server_name localhost host.docker.internal; 8 | 9 | client_max_body_size 1g; 10 | 11 | error_log /var/log/nginx/error.log; 12 | access_log /var/log/nginx/access.log; 13 | 14 | root /var/www/html; 15 | 16 | absolute_redirect off; 17 | 18 | location / { 19 | try_files $uri $uri/ /index.php?$args; 20 | } 21 | 22 | location ~ \.php$ { 23 | try_files $uri =404; 24 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 25 | fastcgi_pass php:9000; 26 | fastcgi_index index.php; 27 | include fastcgi_params; 28 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 29 | fastcgi_param PATH_INFO $fastcgi_path_info; 30 | fastcgi_pass_header Authorization; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/php-config.ini: -------------------------------------------------------------------------------- 1 | display_errors = On 2 | error_reporting = -1 3 | memory_limit = 1024M 4 | post_max_size = 1024M 5 | upload_max_filesize = 1024M 6 | -------------------------------------------------------------------------------- /config/wp-config.php: -------------------------------------------------------------------------------- 1 | array( 11 | 10 => array( 12 | array( 13 | 'accepted_args' => 1, 14 | 'function' => function( $classes ) { 15 | return $classes .= ' mobile'; 16 | }, 17 | ), 18 | ), 19 | ), 20 | ); 21 | 22 | define( 'WP_DEBUG', ! empty( getenv( 'WORDPRESS_DEBUG' ) ) ); 23 | 24 | // Prevent WP-Cron doing its thing during testing. 25 | define( 'DISABLE_WP_CRON', true ); 26 | 27 | // WARNING WARNING WARNING! 28 | // These tests will DROP ALL TABLES in the database with the prefix named below. 29 | // DO NOT use a production database or one that is shared with something else. 30 | define( 'DB_NAME', getenv( 'WORDPRESS_DB_NAME' ) ); 31 | define( 'DB_USER', getenv( 'WORDPRESS_DB_USER' ) ); 32 | define( 'DB_PASSWORD', getenv( 'WORDPRESS_DB_PASSWORD' ) ); 33 | define( 'DB_HOST', getenv( 'WORDPRESS_DB_HOST' ) ); 34 | define( 'DB_CHARSET', 'utf8' ); 35 | define( 'DB_COLLATE', '' ); 36 | 37 | /** 38 | * WordPress Database Table prefix. 39 | * 40 | * You can have multiple installations in one database if you give each 41 | * a unique prefix. Only numbers, letters, and underscores please! 42 | */ 43 | $table_prefix = 'wp_acceptance_'; 44 | 45 | /** Sets up WordPress vars and included files. */ 46 | require_once ABSPATH . 'wp-settings.php'; 47 | -------------------------------------------------------------------------------- /phpstan/extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | bootstrapFiles: 3 | - stubs.php 4 | dynamicConstantNames: 5 | - COOKIE_DOMAIN 6 | - SAVEQUERIES 7 | tmpDir: ../../../../tests/cache/phpstan 8 | -------------------------------------------------------------------------------- /phpstan/stubs.php: -------------------------------------------------------------------------------- 1 |