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
15 changes: 4 additions & 11 deletions .github/fixtures/config/application.subdom.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,10 @@
/**
* Debugging Settings
*/
if ( ( isset( $_ENV['PANTHEON_ENVIRONMENT'] ) && $_ENV['PANTHEON_ENVIRONMENT'] === 'dev' ) || isset( $_ENV['LANDO'] ) ) {
Config::define( 'WP_DEBUG_DISPLAY', true );
Config::define( 'WP_DEBUG_LOG', true );
Config::define( 'SCRIPT_DEBUG', true );
ini_set( 'display_errors', '1' );
} else {
Config::define( 'WP_DEBUG_DISPLAY', false );
Config::define( 'WP_DEBUG_LOG', false );
Config::define( 'SCRIPT_DEBUG', false );
ini_set( 'display_errors', '0' );
}
Config::define( 'WP_DEBUG_DISPLAY', false );
Config::define( 'WP_DEBUG_LOG', true );
Config::define( 'SCRIPT_DEBUG', false );
ini_set( 'display_errors', '0' );

/**
* Allow WordPress to detect HTTPS when used behind a reverse proxy or a load balancer
Expand Down
198 changes: 198 additions & 0 deletions .github/tests/2-rest-url-fix.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env bats

export BATS_LIB_PATH="${BATS_LIB_PATH:-/usr/lib}"
bats_load_library bats-support
bats_load_library bats-assert

# wp wrapper function
_wp() {
terminus wp -- ${SITE_ID}.dev "$@"
}

# Helper function to get REST URL via WP-CLI
get_rest_url() {
_wp eval 'echo get_rest_url();'
}

# Helper function to get home_url path via WP-CLI
get_home_url_path() {
_wp eval 'echo rtrim(parse_url(home_url(), PHP_URL_PATH) ?: "", "/");'
}

# Helper function to check if it's a multisite installation
_is_multisite() {
# This command exits with 0 if it's a network (multisite) installation,
# and 1 otherwise. We suppress output as we only care about the exit code.
if _wp core is-installed --network > /dev/null 2>&1; then
return 0 # true, it is multisite
else
return 1 # false, it is not multisite
fi
}

set_permalinks_to_pretty() {
_wp option update permalink_structure '/%postname%/' --quiet
}

unset_pretty_permalinks() {
_wp option update permalink_structure '' --quiet
}

flush_rewrites() {
_wp rewrite flush --hard --quiet
}

setup_suite() {
# Ensure WP is installed and we are in the right directory
if ! _wp core is-installed; then
echo "WordPress not installed. Run setup script first."
exit 1
fi
}

teardown_test() {
flush_rewrites # Call your helper
}

@test "Check REST URL with default (pretty) permalinks" {
set_permalinks_to_pretty
flush_rewrites

local rest_api_base_path
if _is_multisite; then
rest_api_base_path="/wp/wp-json/"
else
rest_api_base_path="/wp-json/"
fi
SITE_URL="https://dev-${SITE_ID}.pantheonsite.io${rest_api_base_path}"

run curl -s -o /dev/null -w '%{http_code}:%{content_type}' -L "${SITE_URL}"
assert_success "curl command failed to access ${SITE_URL}"
# Assert that the final HTTP status code is 200 (OK) and application/json
assert_output --partial "200:" "Expected HTTP 200 for ${SITE_URL}. Output: $output"
assert_output --partial ":application/json" "Expected Content-Type application/json for ${SITE_URL}. Output: $output"
}

@test "Check REST URL with plain permalinks" {
# Set plain permalinks and flush
unset_pretty_permalinks
flush_rewrites

run get_rest_url
assert_success
# With plain permalinks, expect ?rest_route= based on home_url
# Check if it contains the problematic /wp-json/wp/ segment (it shouldn't)
refute_output --partial "/wp-json/wp/"
# Check if it contains the expected ?rest_route=
assert_output --partial "?rest_route=/"

# Restore pretty permalinks for subsequent tests
set_permalinks_to_pretty
}

@test "Check REST URL with pretty permalinks *before* flush (Simulates new site)" {
# Set pretty permalinks *without* flushing
set_permalinks_to_pretty
# DO NOT FLUSH HERE

local rest_api_base_path

# Check home_url path to confirm /wp setup
if _is_multisite; then
run get_home_url_path
assert_success
assert_output --partial "/wp"
rest_api_base_path="/wp/wp-json/"
else
rest_api_base_path="/wp-json/"
fi

SITE_URL="https://dev-${SITE_ID}.pantheonsite.io${rest_api_base_path}"

run curl -s -o /dev/null -w '%{http_code}:%{content_type}' -L "${SITE_URL}"
assert_success "curl command failed to access ${SITE_URL} (before flush)"
# Assert that the final HTTP status code is 200 (OK) and application/json
# This assumes the fix ensures the correct URL works even before flushing.
assert_output --partial "200:" "Expected HTTP 200 for ${SITE_URL} (before flush). Output: $output"
assert_output --partial ":application/json" "Expected Content-Type application/json for ${SITE_URL} (before flush). Output: $output"
}

@test "Access pretty REST API path directly with plain permalinks active" {
# Set plain permalinks and flush
unset_pretty_permalinks
flush_rewrites

# Construct the pretty-style REST API URL for a specific endpoint
local base_domain="https://dev-${SITE_ID}.pantheonsite.io"
local rest_endpoint_full_path
if _is_multisite; then
# For multisite in /wp/, the REST API base is /wp/wp-json/
rest_endpoint_full_path="/wp/wp-json/wp/v2/posts"
else
# For single site, the REST API base is /wp-json/
rest_endpoint_full_path="/wp-json/wp/v2/posts"
fi
TEST_URL="${base_domain}${rest_endpoint_full_path}"

# Make a curl request to the pretty URL
run curl -s -o /dev/null -w '%{http_code}:%{content_type}' -L "${TEST_URL}"
assert_success "curl command failed for ${TEST_URL}. Output: $output"
# Assert that the final HTTP status code is 200 (OK) and application/json
assert_output --partial "200:" "Expected HTTP 200 for ${TEST_URL}. Output: $output"
assert_output --partial ":application/json" "Expected Content-Type application/json for ${TEST_URL}. Output: $output"

# Restore pretty permalinks for subsequent tests
set_permalinks_to_pretty
}

@test "Validate REST API JSON output for 'hello-world' post (with plain permalinks)" {
unset_pretty_permalinks

# Hardcode known post ID
local POST_ID=1
local base_domain="https://dev-${SITE_ID}.pantheonsite.io"
local rest_api_base_path
if _is_multisite; then
rest_api_base_path="/wp/wp-json/"
else
rest_api_base_path="/wp-json/"
fi
local BASE_URL="${base_domain}${rest_api_base_path}"
local HELLO_WORLD_API_URL="${BASE_URL}wp/v2/posts/${POST_ID}"

# Create temp file for body
local BODY_FILE
BODY_FILE=$(mktemp)

# curl writes body to BODY_FILE, metadata to stdout (captured by 'run')
run curl -s -L -o "$BODY_FILE" \
-w "HTTP_STATUS:%{http_code}\nCONTENT_TYPE:%{content_type}" \
"${HELLO_WORLD_API_URL}"

assert_success "curl command failed for ${HELLO_WORLD_API_URL}. Output: $output"

# Parse and assert metadata from $output
HTTP_STATUS=$(echo "$output" | grep "HTTP_STATUS:" | cut -d: -f2)
CONTENT_TYPE=$(echo "$output" | grep "CONTENT_TYPE:" | cut -d: -f2-)

assert_equal "$HTTP_STATUS" "200" "HTTP status was '$HTTP_STATUS', expected '200'. Full metadata: $output"

echo "$CONTENT_TYPE" | grep -q "application/json"
assert_success "Content-Type was '$CONTENT_TYPE', expected to contain 'application/json'. Full metadata: $output"

JSON_BODY=$(cat "$BODY_FILE")

echo "$JSON_BODY" | jq -e . > /dev/null
assert_success "Response body is not valid JSON. Body: $JSON_BODY"

run jq -e ".id == ${POST_ID}" <<< "$JSON_BODY"
assert_success "JSON .id mismatch. Expected ${POST_ID}. Body: $JSON_BODY"

run jq -e '.slug == "hello-world"' <<< "$JSON_BODY"
assert_success "JSON .slug mismatch. Expected 'hello-world'. Body: $JSON_BODY"

run jq -e '.title.rendered == "Hello world!"' <<< "$JSON_BODY"
assert_success "JSON .title.rendered mismatch. Expected 'Hello world!'. Body: $JSON_BODY"

set_permalinks_to_pretty
}
16 changes: 16 additions & 0 deletions .github/tests/custom-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test as baseTest, expect as baseExpect } from '@playwright/test';

// Extend base test by providing a custom 'page' fixture.
export const test = baseTest.extend({
page: async ({ page }, use) => {
// Set custom headers for all page navigations/requests initiated by the page.
await page.setExtraHTTPHeaders({
'Deterrence-Bypass': 'true',
});
// Continue with the test, providing the modified page fixture.
await use(page);
},
});

// Re-export expect so you can import it from this file as well.
export { baseExpect as expect };
4 changes: 3 additions & 1 deletion .github/tests/wpcm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "./custom-test";

const exampleArticle = "Hello world!";
const siteTitle = process.env.SITE_NAME || "WPCM Playwright Tests";
Expand All @@ -7,6 +7,7 @@ let graphqlEndpoint = process.env.GRAPHQL_ENDPOINT || `${siteUrl}/wp/graphql`;

test("homepage loads and contains example content", async ({ page }) => {
await page.goto(siteUrl);

await expect(page).toHaveTitle(siteTitle);
await expect(page.getByText(exampleArticle)).toHaveText(exampleArticle);
});
Expand All @@ -22,6 +23,7 @@ test("WP REST API is accessible", async ({ request }) => {

test("Hello World post is accessible", async ({ page }) => {
await page.goto(`${siteUrl}/hello-world/'`);

await expect(page).toHaveTitle(`${exampleArticle} – ${siteTitle}`);
// Locate the element containing the desired text
const welcomeText = page.locator('text=Welcome to WordPress');
Expand Down
95 changes: 85 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3']
php-version: ['8.1', '8.2', '8.3', '8.4']

steps:
- uses: actions/checkout@v4
Expand All @@ -39,22 +39,97 @@ jobs:
restore-keys: ${{ runner.os }}-composer-

- name: Check Commits
id: commit_check # Add an ID to access outputs
run: |
# Don't warn about detached head.
git config advice.detachedHead false
git fetch --all
git checkout -b default origin/default
git fetch --all --prune # Fetch all refs and remove stale remote-tracking branches

# Ensure the local 'default' branch reflects 'origin/default'
git checkout -B default origin/default

# Checkout the actual head of the pull request branch
git checkout ${{ github.event.pull_request.head.sha }}
echo "This script does preliminary checks to make sure that the commits in a PR made in this repository are ready for the deploy-public-upstream script. i.e. any given commit modifies either 'normal' or 'non-release' files, never mixed."
bash ${{ github.workspace }}/devops/scripts/check-commits.sh || echo "commit_check_failed=1" >> $GITHUB_ENV

- name: Comment on PR if commit check failed
if: env.commit_check_failed == '1'
echo "Running check-commits.sh to analyze PR commits..."

script_exit_code=0
# Execute the script, capturing all its stderr.
script_stderr_output=$(bash ${{ github.workspace }}/devops/scripts/check-commits.sh 2>&1) || script_exit_code=$?

echo "--- Script Standard Error Output (and stdout if any) ---"
echo "${script_stderr_output}"
echo "--- End Script Output ---"
echo "Script exit code: $script_exit_code"

# Script itself failed (e.g., mixed files in a single commit, forbidden files)
commit_script_failed_output="false"
if [ "$script_exit_code" -ne 0 ]; then
commit_script_failed_output="true"
fi
echo "commit_script_failed=${commit_script_failed_output}" >> $GITHUB_OUTPUT

# Mixture of 'normal' and 'non-release' commit types across the PR
has_normal_commits=$(echo "${script_stderr_output}" | grep -c "is a normal commit" || true)
has_nonrelease_commits=$(echo "${script_stderr_output}" | grep -c "is a non-release commit" || true)

echo "Normal commits found: $has_normal_commits"
echo "Non-release commits found: $has_nonrelease_commits"

mixed_commit_types_in_pr_output="false"
if [ "$has_normal_commits" -gt 0 ] && [ "$has_nonrelease_commits" -gt 0 ]; then
mixed_commit_types_in_pr_output="true"
fi
echo "mixed_commit_types_in_pr=${mixed_commit_types_in_pr_output}" >> $GITHUB_OUTPUT

# Prepare overall error summary for the comment
final_error_summary=""
if [ "${commit_script_failed_output}" == "true" ]; then
# Extract lines that look like errors from the script
script_reported_errors=$(echo "${script_stderr_output}" | grep -E "contains both release and nonrelease changes|contains forbidden files" || true)
if [ -n "${script_reported_errors}" ]; then
final_error_summary+="Script reported the following issues with specific commits:\n${script_reported_errors}\n\n"
else
# If script failed but no specific errors were grepped, include the full output for context
final_error_summary+="The check-commits.sh script failed (exit code $script_exit_code). Full script output for context:\n${script_stderr_output}\n\n"
fi
fi

if [ "${mixed_commit_types_in_pr_output}" == "true" ]; then
final_error_summary+="This PR contains a mixture of 'normal' (release) commits and 'non-release' (internal) commits. This requires careful merging (e.g., rebase and merge) to ensure only 'normal' commits are deployed to public upstream if that's the intent."
fi

echo "final_error_summary<<ERROR_SUMMARY_EOF" >> $GITHUB_OUTPUT
echo -e "${final_error_summary}" >> $GITHUB_OUTPUT
echo "ERROR_SUMMARY_EOF" >> $GITHUB_OUTPUT

- name: Comment on PR if commit check failed or types are mixed
# Trigger if the script failed OR if there's a mix of commit types
if: steps.commit_check.outputs.commit_script_failed == 'true' || steps.commit_check.outputs.mixed_commit_types_in_pr == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ERROR_DETAILS: ${{ steps.commit_check.outputs.final_error_summary }}
run: |
gh pr comment ${{ github.event.pull_request.number }} -b "Hi from your friendly robot! :robot: It looks like there might be commits to both release and non-release files in this PR. Please review and remove any commits that don't belong."
exit 1
COMMENT_MARKER="<!-- GITHUB_ACTIONS_COMMIT_CHECK_COMMENT -->"

# Check for existing comment with the marker
EXISTING_COMMENT_ID=$(gh pr view $PR_NUMBER --json comments -q ".comments[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" || echo "")

if [ -n "$EXISTING_COMMENT_ID" ]; then
echo "Commit check comment already exists (ID: $EXISTING_COMMENT_ID). Skipping new comment."
else
echo "No existing commit check comment found. Posting a new one."
COMMENT_BODY="Hi from your friendly robot! :robot: ${COMMENT_MARKER}
Please review the commit checks for this PR:

> ${ERROR_DETAILS}

If issues are present, please ensure commits modify either 'normal' or 'non-release' files (not a mix within a single commit) and do not contain forbidden files.
If this PR intentionally mixes 'normal' and 'non-release' commit types, remember to use **rebase and merge** rather than **squash** when merging to preserve individual commit integrity for the deploy process."

gh pr comment $PR_NUMBER -b "$COMMENT_BODY"
fi

- name: Check Composer lock file is up to date
run: composer validate --no-check-all
Expand All @@ -77,7 +152,7 @@ jobs:
env:
CI: 1
run: |
bats -p -t .github/tests
bats -p -t .github/tests/1-test-update-php.bats

- name: Create failure status artifact
if: failure()
Expand Down
Loading
Loading