Skip to content

Commit 80ee084

Browse files
authored
Merge pull request #1272 from cloudinary/develop
3.3.8 (develop to uat)
2 parents 55cb077 + e458e62 commit 80ee084

49 files changed

Lines changed: 5915 additions & 4181 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build-docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Setup Node.js
1919
uses: actions/setup-node@v4
2020
with:
21-
node-version: '22'
21+
node-version-file: '.nvmrc'
2222
cache: 'npm'
2323

2424
- name: Install dependencies

‎.github/workflows/ci.yml‎

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
php-version: [ '7.4', '8.3' ]
14+
php-version: [ '7.4', '8.5' ]
1515

1616
steps:
1717
- name: Checkout repository
@@ -25,7 +25,7 @@ jobs:
2525
- name: Setup Node.js
2626
uses: actions/setup-node@v4
2727
with:
28-
node-version: '22'
28+
node-version-file: '.nvmrc'
2929
cache: 'npm'
3030

3131
- name: Cache Composer
@@ -55,7 +55,7 @@ jobs:
5555
- name: Setup PHP
5656
uses: shivammathur/setup-php@v2
5757
with:
58-
php-version: '8.3'
58+
php-version: '8.5'
5959

6060
- name: Cache Composer
6161
uses: actions/cache@v4
@@ -80,30 +80,140 @@ jobs:
8080
- name: Run PHPStan
8181
run: composer phpstan
8282

83+
unit:
84+
name: Unit (PHPUnit)
85+
runs-on: ubuntu-latest
86+
steps:
87+
- name: Checkout repository
88+
uses: actions/checkout@v4
89+
90+
- name: Setup PHP
91+
uses: shivammathur/setup-php@v2
92+
with:
93+
php-version: '8.2'
94+
95+
- name: Setup Node.js
96+
uses: actions/setup-node@v4
97+
with:
98+
node-version-file: '.nvmrc'
99+
100+
- name: Cache Composer
101+
uses: actions/cache@v4
102+
with:
103+
path: ~/.composer/cache
104+
key: ${{ runner.os }}-composer-unit-${{ hashFiles('**/composer.lock') }}
105+
restore-keys: |
106+
${{ runner.os }}-composer-unit-
107+
108+
# This job only needs vendor/bin/phpunit (from Composer) and the wp-env
109+
# CLI. A full `npm ci` pulls ~2,200 packages and has taken anywhere from
110+
# 36s to 7 minutes on hosted runners; @wordpress/env alone is ~400
111+
# packages and installs in ~30s. It is installed into a scratch prefix
112+
# outside the repo so npm does not reconcile against package-lock.json
113+
# and pull the whole tree anyway. The version is read from the lockfile
114+
# so it cannot drift from what developers run locally.
115+
- name: Install Composer dependencies
116+
run: composer install --no-interaction --no-progress
117+
118+
- name: Install wp-env
119+
run: |
120+
version=$(node -p "require('./package-lock.json').packages['node_modules/@wordpress/env'].version")
121+
echo "Installing @wordpress/env@$version"
122+
npm install --prefix "$RUNNER_TEMP/wp-env" --no-audit --no-fund "@wordpress/env@$version"
123+
echo "$RUNNER_TEMP/wp-env/node_modules/.bin" >> "$GITHUB_PATH"
124+
125+
# The wp-env sources directory is deliberately not cached. A restored
126+
# ~/.wp-env carries the previous run's install state, which skipped the
127+
# plugin's activation hook and left the relationships table missing.
128+
- name: Start wp-env
129+
run: wp-env start
130+
131+
- name: Run unit tests
132+
run: wp-env run tests-cli --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit
133+
134+
- name: Stop wp-env
135+
if: always()
136+
run: wp-env stop
137+
83138
e2e:
84139
name: E2E (Playwright)
85140
runs-on: ubuntu-latest
86141
steps:
87142
- name: Checkout repository
88143
uses: actions/checkout@v4
89144

145+
# Not needed by Playwright, but every job that has this step completes
146+
# `npm ci` in ~35s while this job, without it, took 2.5 to 4 minutes on
147+
# the same runs (same npm, same cache hit). setup-php also installs
148+
# Composer, which the postinstall hook calls.
149+
- name: Setup PHP
150+
uses: shivammathur/setup-php@v2
151+
with:
152+
php-version: '8.2'
153+
90154
- name: Setup Node.js
91155
uses: actions/setup-node@v4
92156
with:
93-
node-version: '22'
157+
node-version-file: '.nvmrc'
94158
cache: 'npm'
95159

160+
- name: Cache Composer
161+
uses: actions/cache@v4
162+
with:
163+
path: ~/.composer/cache
164+
key: ${{ runner.os }}-composer-e2e-${{ hashFiles('**/composer.lock') }}
165+
restore-keys: |
166+
${{ runner.os }}-composer-e2e-
167+
168+
# Deliberately not `--ignore-scripts`: with the npm 10 that ships with
169+
# the pinned Node, that flag made this step take 4 to 7 minutes instead
170+
# of ~36s (a known npm 10 reify stall around lifecycle-script nodes).
96171
- name: Install dependencies
97172
run: npm ci
98173

99-
- name: Install Playwright browsers
100-
run: npx playwright install --with-deps chromium
174+
- name: Get Playwright version
175+
id: playwright-version
176+
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
101177

102-
- name: Build assets
103-
run: npm run build
104-
105-
- name: Start wp-env
106-
run: npm run env:start
178+
- name: Cache Playwright browsers
179+
id: playwright-cache
180+
uses: actions/cache@v4
181+
with:
182+
path: ~/.cache/ms-playwright
183+
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
184+
185+
# wp-env spends most of its time pulling Docker images and installing
186+
# WordPress, none of which depends on the Node-side steps. Run it in the
187+
# background while the browser install and asset build proceed, then
188+
# wait for it. All of this has to live in one step because `wait` only
189+
# sees children of the same shell. wp-env output goes to a file and is
190+
# printed afterwards so the interleaved log stays readable.
191+
- name: Start wp-env, install browsers, build assets
192+
env:
193+
PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }}
194+
run: |
195+
npm run env:start > wp-env-start.log 2>&1 &
196+
wp_env_pid=$!
197+
198+
if [ "$PLAYWRIGHT_CACHE_HIT" = "true" ]; then
199+
# Browser binaries came from cache; only the apt packages they
200+
# need are missing on a fresh runner.
201+
npx playwright install-deps chromium
202+
else
203+
npx playwright install --with-deps chromium
204+
fi
205+
206+
npm run build
207+
208+
echo "::group::wp-env start"
209+
if wait "$wp_env_pid"; then
210+
cat wp-env-start.log
211+
echo "::endgroup::"
212+
else
213+
cat wp-env-start.log
214+
echo "::endgroup::"
215+
exit 1
216+
fi
107217
108218
- name: Run E2E tests
109219
env:

‎.github/workflows/deploy-to-wp-org.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
- name: Setup PHP
5151
uses: shivammathur/setup-php@v2
5252
with:
53-
php-version: '8.3'
53+
php-version: '8.5'
5454
tools: composer:v2
5555

5656
- name: Get Composer cache directory

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,9 @@ package/dist
6262
# PHPStan result cache
6363
/.phpstan-cache/
6464

65+
# PHPUnit
66+
/.phpunit.result.cache
67+
/phpunit.xml
68+
6569
# Generated docs (published to gh-pages by workflow)
6670
/docs/

‎.wp-env.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"core": "WordPress/WordPress#7.0",
3-
"phpVersion": "8.2",
2+
"core": "WordPress/WordPress#7.1",
3+
"phpVersion": "8.5",
44
"plugins": ["."],
55
"config": {
66
"WP_DEBUG": true,

‎.wp-env/mu-plugins/analytics-capture.php‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,101 @@
1111
* proceed, which meant every local/CI test run was quietly leaking synthetic
1212
* events (and deactivation "feedback") into the real production collector.
1313
*
14+
* Two additions support running the e2e suite in parallel Playwright
15+
* workers against this single WordPress install: the capture log is
16+
* per-worker (see cld_analytics_capture_worker_marker()), and Admin API
17+
* calls made with the fake e2e credentials are answered locally (see
18+
* cld_e2e_fake_cloud_intercept()).
19+
*
1420
* @package Cloudinary
1521
*/
1622

1723
defined( 'ABSPATH' ) || exit;
1824

1925
/**
20-
* Returns the path to the capture log file.
26+
* Returns the e2e worker marker for the current request, if any.
27+
*
28+
* Playwright runs spec files in parallel workers against this single
29+
* WordPress install. Each worker tags its browser/REST traffic with a
30+
* `cld_e2e_worker` cookie and its WP-CLI calls with a `CLD_E2E_WORKER`
31+
* env var, so every worker gets its own capture log and one worker's
32+
* events (or `--clear`) can't leak into another worker's assertions.
33+
*
34+
* Requests without a marker (manual QA, fire-and-forget loopback threads
35+
* spawned by the sync queue) fall back to the shared, unsuffixed log.
36+
*
37+
* @return string Sanitized marker, or empty string when none is present.
38+
*/
39+
function cld_analytics_capture_worker_marker() {
40+
$marker = '';
41+
42+
// Dev/CI-only mu-plugin with no page cache in front of it, so the VIP
43+
// cache-constraints sniff on $_COOKIE does not apply.
44+
if ( ! empty( $_COOKIE['cld_e2e_worker'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
45+
$marker = wp_unslash( $_COOKIE['cld_e2e_worker'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
46+
} elseif ( false !== getenv( 'CLD_E2E_WORKER' ) && '' !== getenv( 'CLD_E2E_WORKER' ) ) {
47+
$marker = getenv( 'CLD_E2E_WORKER' );
48+
}
49+
50+
return preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $marker );
51+
}
52+
53+
/**
54+
* Returns the path to the capture log file for the current worker.
2155
*
2256
* @return string
2357
*/
2458
function cld_analytics_capture_log_path() {
2559
$upload = wp_upload_dir();
60+
$marker = cld_analytics_capture_worker_marker();
61+
$suffix = '' !== $marker ? '-' . $marker : '';
2662

27-
return $upload['basedir'] . '/analytics-capture.log';
63+
return $upload['basedir'] . '/analytics-capture' . $suffix . '.log';
2864
}
2965

3066
add_filter( 'pre_http_request', 'cld_analytics_capture_intercept', 10, 3 );
67+
add_filter( 'pre_http_request', 'cld_e2e_fake_cloud_intercept', 10, 3 );
68+
69+
/**
70+
* Cloud name used by `fakeCloudinaryConnected()` in tests/e2e/utils/connection.js.
71+
*/
72+
const CLD_E2E_FAKE_CLOUD = 'e2e-fake-cloud';
73+
74+
/**
75+
* Short-circuits Cloudinary Admin API calls made with the fake e2e
76+
* credentials.
77+
*
78+
* Analytics specs fake a connection so `Connect::is_connected()` is true.
79+
* The dashboard then still calls the real Admin API for usage stats and
80+
* per-day history (`Connect::history()` issues one request per day, and the
81+
* 401 responses it gets are never cached because `is_wp_error()` entries
82+
* are refetched). Each real round-trip is ~1s, so one `page=cloudinary`
83+
* load can exceed Playwright's navigation timeout, and parallel workers
84+
* multiply the load. Answer those calls locally with the same 401 the real
85+
* API would return so the plugin's error handling still runs.
86+
*
87+
* @param false|array|WP_Error $preempt Whether to preempt the request.
88+
* @param array $parsed_args Parsed request arguments.
89+
* @param string $url The request URL.
90+
*
91+
* @return false|array|WP_Error
92+
*/
93+
function cld_e2e_fake_cloud_intercept( $preempt, $parsed_args, $url ) {
94+
if ( false === strpos( $url, 'api.cloudinary.com/v1_1/' . CLD_E2E_FAKE_CLOUD . '/' ) ) {
95+
return $preempt;
96+
}
97+
98+
return array(
99+
'headers' => array( 'content-type' => 'application/json' ),
100+
'body' => wp_json_encode( array( 'error' => array( 'message' => 'Invalid credentials (e2e fake cloud)' ) ) ),
101+
'response' => array(
102+
'code' => 401,
103+
'message' => 'Unauthorized',
104+
),
105+
'cookies' => array(),
106+
'filename' => null,
107+
);
108+
}
31109

32110
/**
33111
* Logs outgoing analytics/deactivation-reason requests and preempts them

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog),
3737

3838
### Prerequisites
3939

40-
- [Node.js](https://nodejs.org/) v16+ (see `.nvmrc`)
41-
- [npm](https://www.npmjs.com/) v6.9+
40+
- [Node.js](https://nodejs.org/) v22+ (see `.nvmrc`)
41+
- [npm](https://www.npmjs.com/) v10+
4242
- [Composer](https://getcomposer.org/)
4343
- [Docker](https://www.docker.com/) (required for the WordPress local environment via `wp-env`)
4444

‎composer.json‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
"ext-json": "*"
88
},
99
"require-dev": {
10-
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
11-
"phpcompatibility/phpcompatibility-wp": "dev-master",
12-
"phpcompatibility/php-compatibility": "dev-develop as 9.99.99",
10+
"dealerdirect/phpcodesniffer-composer-installer": "^1.2.1",
11+
"phpcompatibility/phpcompatibility-wp": "^2.1.8",
12+
"phpcompatibility/php-compatibility": "^9.3.5",
1313
"automattic/vipwpcs": "^3.0",
1414
"wp-coding-standards/wpcs": "^3.0",
1515
"phpstan/phpstan": "^2.0",
1616
"szepeviktor/phpstan-wordpress": "^2.0",
1717
"php-stubs/wp-cli-stubs": "^2.10",
18-
"php-stubs/woocommerce-stubs": "^9.0"
18+
"php-stubs/woocommerce-stubs": "^11.0",
19+
"phpunit/phpunit": "^9.6@stable",
20+
"yoast/phpunit-polyfills": "^4.0@stable"
1921
},
2022
"config": {
2123
"platform": {
@@ -34,7 +36,11 @@
3436
],
3537
"phpstan": [
3638
"phpstan analyse --memory-limit=-1"
39+
],
40+
"test": [
41+
"phpunit"
3742
]
3843
},
39-
"minimum-stability": "dev"
44+
"minimum-stability": "dev",
45+
"prefer-stable": true
4046
}

0 commit comments

Comments
 (0)