Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
993d429
chore(deps-dev): bump globals from 17.11.0 to 17.12.0
dependabot[bot] Sep 11, 2026
4d71190
chore(deps): bump @codemirror/state from 6.7.1 to 6.7.4
dependabot[bot] Sep 11, 2026
b5c08a1
chore(deps-dev): bump lint-staged from 17.3.0 to 17.5.0
dependabot[bot] Sep 11, 2026
233d0e0
Fix media uploaded in a non-default WPML language never finishing sync
gabriel-detassigny Sep 15, 2026
382dc40
chore(deps-dev): bump @playwright/test from 1.61.0 to 1.63.0
dependabot[bot] Sep 15, 2026
dd29364
Re-apply the language via wpml_permalink after cleaning the REST URL
gabriel-detassigny Sep 15, 2026
f238710
chore(deps-dev): bump php-stubs/woocommerce-stubs from 11.0.0 to 11.1.0
dependabot[bot] Sep 15, 2026
f3faa09
chore(deps-dev): bump phpstan/phpstan from 2.2.12 to 2.2.13
dependabot[bot] Sep 15, 2026
b472240
Trim rest_url() docblock
gabriel-detassigny Sep 15, 2026
3890055
feat(env): Serve local wp-env over HTTPS
PatelUtkarsh Sep 15, 2026
3fc6cc8
Merge pull request #1281 from cloudinary/bugfix/wpml-rest-loopback-url
gabrielcld2 Sep 17, 2026
da7e7a9
Merge pull request #1274 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 17, 2026
92551cc
Merge pull request #1275 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 17, 2026
af6a98e
Merge pull request #1276 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 17, 2026
c818d5f
Merge pull request #1278 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 17, 2026
a3102f4
Merge pull request #1279 from cloudinary/dependabot/composer/develop/…
gabrielcld2 Sep 17, 2026
6655e65
Merge pull request #1280 from cloudinary/dependabot/composer/develop/…
gabrielcld2 Sep 17, 2026
3c91c85
Update assets
gabriel-detassigny Sep 17, 2026
f12a8a2
chore(deps-dev): bump phpstan/phpstan from 2.2.13 to 2.2.14
dependabot[bot] Sep 18, 2026
83d4a51
chore(deps-dev): bump the wordpress group with 13 updates
dependabot[bot] Sep 18, 2026
500bd08
chore(deps-dev): bump lint-staged from 17.5.0 to 17.5.1
dependabot[bot] Sep 18, 2026
447a8a7
chore(deps-dev): bump cssnano from 7.1.9 to 9.0.4
dependabot[bot] Sep 18, 2026
c111fec
fix(env): Scope proxy scripts to this project and its ports
PatelUtkarsh Sep 21, 2026
dc3c977
fix(ci): Install unit job dependencies from the lockfile
PatelUtkarsh Sep 21, 2026
8b922b0
Merge pull request #1282 from cloudinary/dependabot/composer/develop/…
gabrielcld2 Sep 22, 2026
cb972e7
Merge pull request #1283 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 22, 2026
03e5454
Merge pull request #1284 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 22, 2026
5b0e140
Merge pull request #1286 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 22, 2026
6c150bf
chore(deps-dev): bump webpack from 5.109.0 to 5.111.1
dependabot[bot] Sep 22, 2026
3d3bcda
Merge pull request #1285 from cloudinary/dependabot/npm_and_yarn/deve…
gabrielcld2 Sep 22, 2026
1484b73
fix(env): Respect custom wp-env ports in loopback and URL rewrite
PatelUtkarsh Sep 22, 2026
45987e5
Merge pull request #1287 from cloudinary/feature/local-https-proxy
gabrielcld2 Sep 22, 2026
88f7690
Update build assets
gabriel-detassigny Sep 22, 2026
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
52 changes: 31 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Cache Composer
uses: actions/cache@v4
Expand All @@ -105,35 +106,40 @@ jobs:
restore-keys: |
${{ runner.os }}-composer-unit-

# This job only needs vendor/bin/phpunit (from Composer) and the wp-env
# CLI. A full `npm ci` pulls ~2,200 packages and has taken anywhere from
# 36s to 7 minutes on hosted runners; @wordpress/env alone is ~400
# packages and installs in ~30s. It is installed into a scratch prefix
# outside the repo so npm does not reconcile against package-lock.json
# and pull the whole tree anyway. The version is read from the lockfile
# so it cannot drift from what developers run locally.
- name: Install Composer dependencies
run: composer install --no-interaction --no-progress

- name: Install wp-env
run: |
version=$(node -p "require('./package-lock.json').packages['node_modules/@wordpress/env'].version")
echo "Installing @wordpress/env@$version"
npm install --prefix "$RUNNER_TEMP/wp-env" --no-audit --no-fund "@wordpress/env@$version"
echo "$RUNNER_TEMP/wp-env/node_modules/.bin" >> "$GITHUB_PATH"
# This job only needs vendor/bin/phpunit and the wp-env CLI, so it used to
# install @wordpress/env on its own into a scratch prefix rather than run
# a full `npm ci`. That install resolved dependency ranges fresh against
# npm instead of obeying package-lock.json, which made the job depend on
# whatever upstream had published that day: a broken @wp-playground/cli
# release, pulled in transitively by @wordpress/env, failed every run
# while the lockfile-based jobs were unaffected.
#
# `npm ci` installs the locked tree, so the job can no longer break
# because of a third-party release. The postinstall hook runs
# `patch-package && composer install`, which is what provides
# vendor/bin/phpunit, so this single step covers both toolchains.
#
# Deliberately not `--ignore-scripts`, for the same reason as the e2e
# job below: besides skipping the Composer install, that flag made this
# step take 1m30s instead of ~36s from a cold cache.
- name: Install dependencies
run: npm ci

# The wp-env sources directory is deliberately not cached. A restored
# ~/.wp-env carries the previous run's install state, which skipped the
# plugin's activation hook and left the relationships table missing.
# .wp-env.ci.json omits the HTTPS URLs and the proxy lifecycle script
# that .wp-env.json uses for local development. Runners have no local CA
# and no proxy container, so they serve the site over plain HTTP.
- name: Start wp-env
run: wp-env start
run: npx wp-env start --config .wp-env.ci.json

- name: Run unit tests
run: wp-env run tests-cli --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit
run: npx wp-env run tests-cli --config .wp-env.ci.json --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit

- name: Stop wp-env
if: always()
run: wp-env stop
run: npx wp-env stop --config .wp-env.ci.json

e2e:
name: E2E (Playwright)
Expand Down Expand Up @@ -192,7 +198,7 @@ jobs:
env:
PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }}
run: |
npm run env:start > wp-env-start.log 2>&1 &
npx wp-env start --config .wp-env.ci.json > wp-env-start.log 2>&1 &
wp_env_pid=$!

if [ "$PLAYWRIGHT_CACHE_HIT" = "true" ]; then
Expand All @@ -215,14 +221,18 @@ jobs:
exit 1
fi

# Runners have no local CA and no HTTPS proxy, so the suite talks to
# wp-env's published port directly over plain HTTP. WP_BASE_URL is the
# single switch for this; see tests/e2e/playwright.config.js.
- name: Run E2E tests
env:
CLOUDINARY_E2E_URL: ${{ secrets.CLOUDINARY_E2E_URL }}
WP_BASE_URL: http://localhost:8889
run: npm run test:e2e

- name: Stop wp-env
if: always()
run: npm run env:stop
run: npx wp-env stop --config .wp-env.ci.json

- name: Upload Playwright artifacts
if: failure()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ coverage/html/
# wp-env personal overrides
.wp-env.override.json

# Locally generated TLS certificates and CA for the wp-env HTTPS proxy
/.wp-env/certs/

# IDE
.vscode

Expand Down
26 changes: 26 additions & 0 deletions .wp-env.ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"core": "WordPress/WordPress#7.1",
"phpVersion": "8.5",
"plugins": ["."],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"SCRIPT_DEBUG": true
},
"mappings": {
"wp-content/mu-plugins": "./.wp-env/mu-plugins"
},
"lifecycleScripts": {
"afterStart": "./.wp-env/scripts/fix-loopback.sh"
},
"env": {
"tests": {
"phpVersion": "8.2",
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"SCRIPT_DEBUG": true
}
}
}
}
16 changes: 13 additions & 3 deletions .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"SCRIPT_DEBUG": true
"SCRIPT_DEBUG": true,
"WP_HOME": "https://cloudinary.local.wpenv.net",
"WP_SITEURL": "https://cloudinary.local.wpenv.net",
"FORCE_SSL_ADMIN": true,
"WP_CONTENT_URL": "https://cloudinary.local.wpenv.net/wp-content",
"WP_PLUGIN_URL": "https://cloudinary.local.wpenv.net/wp-content/plugins"
},
"mappings": {
"wp-content/mu-plugins": "./.wp-env/mu-plugins"
},
"lifecycleScripts": {
"afterStart": "./.wp-env/scripts/fix-loopback.sh"
"afterStart": "./.wp-env/scripts/after-start.sh"
},
"env": {
"tests": {
"phpVersion": "8.2",
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"SCRIPT_DEBUG": true
"SCRIPT_DEBUG": true,
"WP_HOME": "https://tests.cloudinary.local.wpenv.net",
"WP_SITEURL": "https://tests.cloudinary.local.wpenv.net",
"FORCE_SSL_ADMIN": true,
"WP_CONTENT_URL": "https://tests.cloudinary.local.wpenv.net/wp-content",
"WP_PLUGIN_URL": "https://tests.cloudinary.local.wpenv.net/wp-content/plugins"
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions .wp-env/docker/mkcert/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM golang:1.24-alpine

# Set the version tag to build.
ENV MKCERT_VERSION="v1.4.4"

RUN apk add --no-cache git

RUN git clone https://github.com/FiloSottile/mkcert /go/mkcert \
&& cd /go/mkcert \
&& git checkout "tags/$MKCERT_VERSION" -b "build/$MKCERT_VERSION" \
&& go build -ldflags "-X main.Version=$MKCERT_VERSION" -o /bin/mkcert

# mkcert reads and writes its CA here. The compose file mounts the project's
# certificate directory over it, so both the CA and the issued certificates
# stay inside the repository instead of the developer's global mkcert store.
WORKDIR /root/.local/share/mkcert
105 changes: 105 additions & 0 deletions .wp-env/mu-plugins/https-proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php
/**
* HTTPS proxy awareness — local dev helper mu-plugin.
*
* The local environment terminates TLS in an nginx container that forwards to
* wp-env over plain HTTP (see .wp-env/proxy/). Apache therefore sees an HTTP
* request, and without this mu-plugin `is_ssl()` returns false even though the
* browser is on HTTPS. That mismatch causes an admin redirect loop with
* FORCE_SSL_ADMIN enabled, drops the `secure` flag from auth cookies, and makes
* the plugin's own scheme checks take the wrong branch:
*
* - php/class-delivery.php builds delivery URLs from `is_ssl()`.
* - php/class-media.php sets the cookie `secure` flag from `is_ssl()`.
*
* The fix is the standard reverse-proxy one: promote the forwarded scheme onto
* $_SERVER before WordPress reads it. On a real host this would live in
* wp-config.php, but wp-env generates that file and rewrites it on every start,
* so it goes here instead. mu-plugins load from wp-settings.php before
* wp_cookie_constants() and wp_ssl_constants() run, which is early enough for
* both to see the corrected value.
*
* Only trusted because the proxy is the sole route into these containers in
* local development. Never ship this pattern to production without pinning the
* trusted proxy address.
*
* @package Cloudinary
*/

defined( 'ABSPATH' ) || exit;

if (
isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
'https' === strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) )
) {
$_SERVER['HTTPS'] = 'on';

// Keep SERVER_PORT consistent with the scheme. WordPress appends a non
// standard port to generated URLs, and the unforwarded 80 here would
// produce links such as https://example.test:80/.
if ( isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ) {
$_SERVER['SERVER_PORT'] = absint( $_SERVER['HTTP_X_FORWARDED_PORT'] );
}
}

/**
* Removes the wp-env port from a site URL.
*
* The wp-env tool appends its published port to WP_HOME and WP_SITEURL when it
* writes wp-config.php, and there is no configuration option to prevent it.
* See postProcessConfig() in @wordpress/env/lib/config/post-process-config.js.
*
* The proxy serves the site on the standard HTTPS port, so the appended 8888 or
* 8889 is wrong: it leaks into redirects, canonical URLs, asset URLs and the
* cookie path. Stripping it here is the last chance to correct the value,
* because the constants are already defined by the time mu-plugins load.
*
* The port to remove is not hard-coded, because wp-env lets developers change
* it through WP_ENV_PORT or a "port" key in the config files. The origin is
* taken from WP_CONTENT_URL instead, which .wp-env.json defines as the intended
* public URL and which wp-env never rewrites. Any port on the incoming URL is
* then replaced with whatever that constant says, so a custom wp-env port is
* handled without this file knowing about it.
*
* This filter cannot fix asset URLs. wp_plugin_directory_constants() defines
* WP_CONTENT_URL and WP_PLUGIN_URL from get_option( 'siteurl' ) at
* wp-settings.php line 497, ten lines before mu-plugins load, so plugin CSS and
* JS would keep the port. Those two constants are therefore set explicitly in
* .wp-env.json, where wp-env leaves them alone because it only rewrites
* WP_HOME, WP_SITEURL and WP_TESTS_DOMAIN.
*
* @param string $url The home or site URL.
* @return string The URL with the wp-env port replaced by the public origin.
*/
function cld_strip_wp_env_port( $url ) {
if ( ! is_string( $url ) || ! defined( 'WP_CONTENT_URL' ) ) {
return $url;
}

$origin = wp_parse_url( WP_CONTENT_URL );

if ( empty( $origin['scheme'] ) || empty( $origin['host'] ) ) {
return $url;
}

$parts = wp_parse_url( $url );

// Only rewrite URLs that point at the same host, so an unrelated URL
// passing through these filters is left alone.
if ( empty( $parts['host'] ) || $parts['host'] !== $origin['host'] ) {
return $url;
}

$public = $origin['scheme'] . '://' . $origin['host'];

if ( ! empty( $origin['port'] ) ) {
$public .= ':' . $origin['port'];
}

$path = isset( $parts['path'] ) ? $parts['path'] : '';

return $public . $path;
}

add_filter( 'option_home', 'cld_strip_wp_env_port', 20 );
add_filter( 'option_siteurl', 'cld_strip_wp_env_port', 20 );
45 changes: 45 additions & 0 deletions .wp-env/proxy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# TLS proxy in front of the wp-env environment.
#
# Started and stopped by ../scripts/proxy-up.sh and ../scripts/proxy-down.sh,
# which export the variables used below from ../scripts/config.sh. This is a
# separate compose project from wp-env's own, so `wp-env destroy` cannot remove
# it and `wp-env start` cannot overwrite it.

services:
# Builds mkcert and issues the certificate, then exits. The proxy waits for a
# successful exit so it never starts without a certificate on disk.
mkcert:
build: ../docker/mkcert
volumes:
- ../certs:/root/.local/share/mkcert
# The working directory is the mounted certificate directory, so both the
# CA and the issued pair land in .wp-env/certs/. The wildcard covers the
# tests host name.
command:
- sh
- -c
- test -f "$$DEV_HOST.crt" || mkcert -cert-file "$$DEV_HOST.crt" -key-file "$$DEV_HOST.key" "$$DEV_HOST" "*.$$DEV_HOST"
environment:
DEV_HOST: ${DEV_HOST}

proxy:
image: nginx:1.29-alpine
depends_on:
mkcert:
condition: service_completed_successfully
ports:
- '${PROXY_HTTP_PORT}:${PROXY_HTTP_PORT}'
- '${PROXY_HTTPS_PORT}:${PROXY_HTTPS_PORT}'
volumes:
- ../certs:/etc/nginx/certs:ro
- ./nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
environment:
DEV_HOST: ${DEV_HOST}
TESTS_HOST: ${TESTS_HOST}
PROXY_HTTP_PORT: ${PROXY_HTTP_PORT}
PROXY_HTTPS_PORT: ${PROXY_HTTPS_PORT}
WP_ENV_PORT: ${WP_ENV_PORT}
WP_ENV_TESTS_PORT: ${WP_ENV_TESTS_PORT}
extra_hosts:
- 'host.docker.internal:host-gateway'
restart: unless-stopped
Loading
Loading