Skip to content
Open
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
20 changes: 20 additions & 0 deletions .github/actions/smoke-check/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Smoke check a WordPress installation'
description: 'Serves an installed single site with PHP built-in web server and checks that its front end, REST API, and admin respond.'

inputs:
admin-user:
description: 'The username of an administrator account on the site.'
required: true
admin-password:
description: 'The password of the administrator account.'
required: true

runs:
using: 'composite'
steps:
- name: Serve the site and check it
shell: bash
run: bash "${GITHUB_ACTION_PATH}/smoke-check.sh"
env:
WP_ADMIN_USER: ${{ inputs.admin-user }}
WP_ADMIN_PASSWORD: ${{ inputs.admin-password }}
117 changes: 117 additions & 0 deletions .github/actions/smoke-check/smoke-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/bin/bash
#
# Smoke checks an installed WordPress single site over HTTP.
#
# Runs from the root of the installation, with WP-CLI available. Multisite is not supported because
# a network's domain lives in wp-config.php and the database, not just an option.

set -euo pipefail

: "${RUNNER_TEMP:?}" "${WP_ADMIN_USER:?}" "${WP_ADMIN_PASSWORD:?}"

site_port=8889
site_url="http://127.0.0.1:${site_port}"
server_log="${RUNNER_TEMP}/server.log"
jar="${RUNNER_TEMP}/cookies.txt"
: > "${jar}"
response="${RUNNER_TEMP}/response.txt"
code="${RUNNER_TEMP}/http-code.txt"
http_code=''

version="$(wp core version)"

# Nothing undoes these, so the site is only good for this check afterwards.
wp option update home "${site_url}"
wp option update siteurl "${site_url}"
# A fresh install shows posts here already, but the front page check below needs it.
wp option update show_on_front posts
# Both make requests no check asked for, and the update checks call api.wordpress.org.
wp config set DISABLE_WP_CRON true --raw
wp config set WP_HTTP_BLOCK_EXTERNAL true --raw

# opcache counts php -S as a web SAPI, so the runner's JIT runs here and segfaults.
php -d opcache.jit=disable -S "127.0.0.1:${site_port}" -t . > "${server_log}" 2>&1 &
server_pid=$!

stop_server() {
local status=$?
kill "${server_pid}" 2> /dev/null || true
wait "${server_pid}" 2> /dev/null || true
[ "${status}" -eq 0 ] || cat "${server_log}" || true
exit "${status}"
}
trap stop_server EXIT

# No connection prints 000 and exits 7, which set -e would take as fatal.
deadline=$(( SECONDS + 30 ))
while :; do
ready_code="$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "${site_url}/" || true)"
[ "${ready_code}" = '000' ] || break
[ "${SECONDS}" -lt "${deadline}" ] || { echo '::error::the server never answered'; exit 1; }
sleep 1
done

fail() {
echo "::error::${1}"
tail -n 20 "${response}"
exit 1
}

# An HTTP error returns to the caller. No reply at all is fatal.
fetch() {
local path="${1}" status=0
shift
: > "${response}"
curl --silent --show-error --max-time 30 --output "${response}" \
--write-out '%{http_code}' --cookie "${jar}" --cookie-jar "${jar}" \
"$@" "${site_url}${path}" > "${code}" || status=$?
http_code="$(cat "${code}")"
[ "${status}" -eq 0 ] || [ "${status}" -eq 22 ] \
|| fail "${path} got no reply and curl exited ${status}"
return "${status}"
}

check() {
local path="${1}" marker status=0
shift
fetch "${path}" --fail-with-body || status=$?
[ "${status}" -eq 0 ] || fail "${path} returned HTTP ${http_code}"
for marker in "$@"; do
grep -qF -e "${marker}" "${response}" || fail "${path} did not contain: ${marker}"
done
echo "ok ${path}"
}

# A fatal mid-page still returns 200, so every check also asks for the closing tag.
check '/' "content=\"WordPress ${version}\"" 'Hello world!' '</html>'

fetch '/?rest_route=/' --fail-with-body || fail "/?rest_route=/ returned HTTP ${http_code}"
jq -e --arg url "${site_url}" '.url == $url and ( .namespaces | index( "wp/v2" ) )' "${response}" > /dev/null \
|| fail 'the REST index was not the expected JSON'
echo 'ok /?rest_route=/'

# It prints this for an empty database too, which the checks above rule out.
check '/wp-admin/upgrade.php' 'No Update Required' '</html>'

fetch '/?p=99999999' || true
[ "${http_code}" = '404' ] || fail "a missing post returned ${http_code}, expected 404"
grep -qF '</html>' "${response}" || fail 'the 404 page was cut short'
echo 'ok 404 handling'

login_status=0
fetch '/wp-login.php' --fail-with-body \
--data-urlencode "log=${WP_ADMIN_USER}" --data-urlencode "pwd=${WP_ADMIN_PASSWORD}" || login_status=$?
[ "${login_status}" -eq 0 ] || fail "logging in returned HTTP ${http_code}"
grep -qF 'wordpress_logged_in_' "${jar}" || fail 'logging in did not set an authentication cookie'
echo 'ok login'

# The admin bar renders only for a logged-in user.
check '/wp-admin/' 'id="wpadminbar"' '</html>'

# Catches a fatal during shutdown. An empty log would let the grep pass silently.
[ -s "${server_log}" ] || { echo '::error::the server wrote no log'; exit 1; }
if grep -qE 'Fatal error|Uncaught|Segmentation fault' "${server_log}"; then
echo '::error::the server logged a fatal error'
exit 1
fi
echo 'ok server log'
30 changes: 28 additions & 2 deletions .github/workflows/install-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ on:
- trunk
# Always test the workflow after it's updated.
paths:
- '.github/actions/smoke-check/**'
- '.github/workflows/install-testing.yml'
- '.version-support-*.json'
- '.github/workflows/reusable-support-json-reader-v1.yml'
pull_request:
# Always test the workflow when changes are suggested.
paths:
- '.github/actions/smoke-check/**'
- '.version-support-*.json'
- '.github/workflows/install-testing.yml'
- '.github/workflows/reusable-support-json-reader-v1.yml'
Expand Down Expand Up @@ -60,14 +62,19 @@ jobs:
# - Downloads the specified version of WordPress.
# - Creates a `wp-config.php` file.
# - Installs WordPress.
# - Serves the installed site and confirms it renders.
install-tests-mysql:
name: WP ${{ inputs.wp-version || 'nightly' }} / PHP ${{ matrix.php }} / ${{ 'mariadb' == matrix.db-type && 'MariaDB' || 'MySQL' }} ${{ matrix.db-version }}${{ matrix.multisite && ' multisite' || '' }}
permissions:
contents: read
runs-on: ${{ matrix.os }}
if: ${{ github.repository == 'WordPress/wordpress-develop' }}
timeout-minutes: 10
timeout-minutes: 15
needs: [ build-test-matrix ]

env:
WP_ADMIN_USER: admin
WP_ADMIN_PASSWORD: password
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -135,7 +142,26 @@ jobs:
DB_PORT: ${{ job.services.database.ports['3306'] }}

- name: Install WordPress
run: wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} --url=http://localhost/ --title="Upgrade Test" --admin_user=admin --admin_password=password --admin_email=me@example.org --skip-email
run: |
wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} \
--url=http://localhost/ --title="Install Test" --admin_user="${WP_ADMIN_USER}" \
--admin_password="${WP_ADMIN_PASSWORD}" --admin_email=me@example.org --skip-email

- name: Check out the smoke check action
if: ${{ ! matrix.multisite }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: ci-tools
sparse-checkout: .github/actions
persist-credentials: false

- name: Post-install smoke check
# A network's domain lives in wp-config.php and the database, not just an option.
if: ${{ ! matrix.multisite }}
uses: ./ci-tools/.github/actions/smoke-check
with:
admin-user: ${{ env.WP_ADMIN_USER }}
admin-password: ${{ env.WP_ADMIN_PASSWORD }}

slack-notifications:
name: Slack Notifications
Expand Down
117 changes: 12 additions & 105 deletions .github/workflows/reusable-upgrade-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,111 +141,18 @@ jobs:
- name: Post-upgrade version check
run: wp core version

- name: Check out the smoke check action
if: ${{ ! inputs.multisite }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: ci-tools
sparse-checkout: .github/actions
persist-credentials: false

- name: Post-upgrade smoke check
# A network's domain lives in wp-config.php and the database, not just an option.
if: ${{ ! inputs.multisite }}
run: |
site_url="http://127.0.0.1:${SITE_PORT}"
server_log="${RUNNER_TEMP}/server.log"
jar="${RUNNER_TEMP}/cookies.txt"
response="${RUNNER_TEMP}/response.txt"
code="${RUNNER_TEMP}/http-code.txt"
http_code=''

# Nothing undoes these, so this step stays last.
wp option update home "${site_url}"
wp option update siteurl "${site_url}"
# Both make requests no check asked for, and the update checks call api.wordpress.org.
wp config set DISABLE_WP_CRON true --raw
wp config set WP_HTTP_BLOCK_EXTERNAL true --raw

# opcache counts php -S as a web SAPI, so the runner's JIT runs here and segfaults.
php -d opcache.jit=disable -S "127.0.0.1:${SITE_PORT}" -t . > "${server_log}" 2>&1 &
server_pid=$!

stop_server() {
local status=$?
kill "${server_pid}" 2> /dev/null || true
wait "${server_pid}" 2> /dev/null || true
[ "${status}" -eq 0 ] || cat "${server_log}" || true
exit "${status}"
}
trap stop_server EXIT

# No connection prints 000 and exits 7, which set -e would take as fatal.
deadline=$(( SECONDS + 30 ))
while :; do
ready_code="$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "${site_url}/" || true)"
[ "${ready_code}" = '000' ] || break
[ "${SECONDS}" -lt "${deadline}" ] || { echo '::error::the server never answered'; exit 1; }
sleep 1
done

fail() {
echo "::error::${1}"
tail -n 20 "${response}"
exit 1
}

# An HTTP error returns to the caller. No reply at all is fatal.
fetch() {
local path="${1}" status=0
shift
: > "${response}"
curl --silent --show-error --max-time 30 --output "${response}" \
--write-out '%{http_code}' --cookie "${jar}" --cookie-jar "${jar}" \
"$@" "${site_url}${path}" > "${code}" || status=$?
http_code="$(cat "${code}")"
[ "${status}" -eq 0 ] || [ "${status}" -eq 22 ] \
|| fail "${path} got no reply and curl exited ${status}"
return "${status}"
}

check() {
local path="${1}" marker status=0
shift
fetch "${path}" --fail-with-body || status=$?
[ "${status}" -eq 0 ] || fail "${path} returned HTTP ${http_code}"
for marker in "$@"; do
grep -qF -e "${marker}" "${response}" || fail "${path} did not contain: ${marker}"
done
echo "ok ${path}"
}

version="$(wp core version)"

# A fatal mid-page still returns 200, so every check also asks for the closing tag.
check '/' "content=\"WordPress ${version}\"" 'Hello world!' '</html>'

fetch '/?rest_route=/' --fail-with-body || fail "/?rest_route=/ returned HTTP ${http_code}"
jq -e '.name == "Upgrade Test"' "${response}" > /dev/null \
|| fail 'the REST index was not the expected JSON'
echo 'ok /?rest_route=/'

# It prints this for an empty database too, which the checks above rule out.
check '/wp-admin/upgrade.php' 'No Update Required' '</html>'

fetch '/?p=99999999' || true
[ "${http_code}" = '404' ] || fail "a missing post returned ${http_code}, expected 404"
grep -qF '</html>' "${response}" || fail 'the 404 page was cut short'
echo 'ok 404 handling'

login_status=0
fetch '/wp-login.php' --fail-with-body \
--data-urlencode "log=${WP_ADMIN_USER}" --data-urlencode "pwd=${WP_ADMIN_PASSWORD}" || login_status=$?
[ "${login_status}" -eq 0 ] || fail "logging in returned HTTP ${http_code}"
grep -qF 'wordpress_logged_in_' "${jar}" || fail 'logging in did not set an authentication cookie'
echo 'ok login'

# The admin bar renders only for a logged-in user.
check '/wp-admin/' 'id="wpadminbar"' '</html>'

# Catches a fatal during shutdown. An empty log would let the grep pass silently.
[ -s "${server_log}" ] || { echo '::error::the server wrote no log'; exit 1; }
if grep -qE 'Fatal error|Uncaught|Segmentation fault' "${server_log}"; then
echo '::error::the server logged a fatal error'
exit 1
fi
echo 'ok server log'
env:
SITE_PORT: 8889
uses: ./ci-tools/.github/actions/smoke-check
with:
admin-user: ${{ env.WP_ADMIN_USER }}
admin-password: ${{ env.WP_ADMIN_PASSWORD }}
4 changes: 4 additions & 0 deletions .github/workflows/reusable-workflow-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ jobs:
with:
args: "-color -verbose"

# actionlint shellchecks inline `run:` blocks, but does not read composite actions.
- name: Run shellcheck on composite action scripts
run: find .github -path '.github/actions/*' -name '*.sh' -print0 | xargs -0 -r shellcheck

# Runs the Zizmor GitHub Action workflow file linter.
#
# See https://github.com/zizmorcore/zizmor
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/upgrade-develop-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
# Any change to a source PHP file should run checks.
- 'src/**.php'
# Confirm any changes to relevant workflow files.
- '.github/actions/smoke-check/**'
- '.github/workflows/upgrade-develop-testing.yml'
- '.github/workflows/reusable-upgrade-testing.yml'
pull_request:
Expand All @@ -24,6 +25,7 @@ on:
# Any change to a source PHP file should run checks.
- 'src/**.php'
# Confirm any changes to relevant workflow files.
- '.github/actions/smoke-check/**'
- '.github/workflows/upgrade-develop-testing.yml'
- '.github/workflows/reusable-upgrade-testing.yml'
workflow_dispatch:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/upgrade-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- trunk
# Always test the workflow after it's updated.
paths:
- '.github/actions/smoke-check/**'
- '.github/workflows/upgrade-testing.yml'
- '.github/workflows/reusable-upgrade-testing.yml'
pull_request:
Expand All @@ -17,6 +18,7 @@ on:
- trunk
# Always test the workflow when changes are suggested.
paths:
- '.github/actions/smoke-check/**'
- '.github/workflows/upgrade-testing.yml'
- '.github/workflows/reusable-upgrade-testing.yml'
workflow_dispatch:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/workflow-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ on:
- '[7-9].[0-9]'
paths:
# Only run when changes are made to workflow files.
- '.github/actions/**'
- '.github/workflows/**'
pull_request:
branches:
- trunk
- '[0-9].[0-9]'
paths:
# Only run when changes are made to workflow files.
- '.github/actions/**'
- '.github/workflows/**'
workflow_dispatch:

Expand Down
Loading