diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml index 169abe20006a4..73dd291b56fd9 100644 --- a/.github/workflows/reusable-upgrade-testing.yml +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -56,6 +56,7 @@ jobs: # - Updates to the version of WordPress being tested. # - Updates the database. # - Checks the version of WordPress after the upgrade. + # - Serves the upgraded site and confirms it renders. upgrade-tests: name: PHP ${{ inputs.php }} with ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }} permissions: @@ -63,6 +64,10 @@ jobs: runs-on: ${{ inputs.os }} timeout-minutes: 20 + env: + WP_ADMIN_USER: admin + WP_ADMIN_PASSWORD: password + services: database: image: ${{ inputs.db-type }}:${{ inputs.db-version }} @@ -97,8 +102,8 @@ jobs: - name: Install WordPress run: | wp core ${{ inputs.multisite && 'multisite-install' || 'install' }} \ - --url=http://localhost/ --title="Upgrade Test" --admin_user=admin \ - --admin_password=password --admin_email=me@example.org --skip-email + --url=http://localhost/ --title="Upgrade Test" --admin_user="${WP_ADMIN_USER}" \ + --admin_password="${WP_ADMIN_PASSWORD}" --admin_email=me@example.org --skip-email - name: Pre-upgrade version check run: wp core version @@ -135,3 +140,124 @@ jobs: - name: Post-upgrade version check run: wp core version + + - name: Post-upgrade smoke check + # A network keeps its domain 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='' + + # None of these three are undone, so this step has to stay last. + wp option update home "${site_url}" + wp option update siteurl "${site_url}" + # A cron spawn and the update checks both make requests no check asked for, and the + # update checks leave the runner for api.wordpress.org. + wp config set DISABLE_WP_CRON true --raw + wp config set WP_HTTP_BLOCK_EXTERNAL true --raw + + # opcache counts the built-in server as a web SAPI, not as the CLI, so it runs the + # JIT that the runner turns on and stock PHP does not. That JIT crashed the server + # on 38 of 1600 runs. With it off, none of 1600 crashed. + 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 + + # A poll that cannot connect prints 000 and exits 7, which would end the step here. + 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 + + # Prints what came back and fails the step. + fail() { + echo "::error::${1}" + tail -n 20 "${response}" + exit 1 + } + + # Requests a path, leaving the body in ${response} and the status in ${http_code}. + # An HTTP error is a result and comes back to the caller. No reply at all is not: + # the server is gone, and the log the trap prints says why. + 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}" + } + + # Fails the step unless the response arrives whole and contains every given string. + 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 after the first bytes leaves a 200 holding half a page, so every check also + # asks for the end of the document. Unlike an error string, that cannot be reworded. + check '/' "content=\"WordPress ${version}\"" '' + check '/?p=1' 'Hello world!' '' + + 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=/' + + # The page prints this for a database that needs no upgrade and for one that holds no + # install at all. The checks above have already ruled the second one out. + check '/wp-admin/upgrade.php' 'No Update Required' '' + check '/wp-login.php' 'id="loginform"' '' + + fetch '/?p=99999999' || true + [ "${http_code}" = '404' ] || fail "a missing post returned ${http_code}, expected 404" + grep -qF '' "${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 only renders for a logged-in user, and /wp-admin/ redirects when logged out. + check '/wp-admin/' 'id="wpadminbar"' '' + + # A fatal that lands after the markers, or during shutdown, still leaves a broken site. + # The server always writes a start line, so an empty log means the check reads nothing. + [ -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