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
7 changes: 6 additions & 1 deletion .github/workflows/agent-task-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths:
- ".github/scripts/run-agent-task/**"
- ".github/workflows/agent-task-contracts.yml"
- ".github/workflows/build-php-wasm-mysqli-poll-overlay.yml"
- ".github/workflows/run-agent-task.yml"
- "contracts/agent-task-workflow-request.fixture.json"
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
Expand Down Expand Up @@ -48,10 +49,12 @@ on:
- "packages/runtime-core/src/bounded-runtime-plan.ts"
- "tests/bounded-*.test.ts"
- "tests/disposable-mysql-mysqli.integration.test.ts"
- "tests/mysqli-poll.integration.test.ts"
push:
paths:
- ".github/scripts/run-agent-task/**"
- ".github/workflows/agent-task-contracts.yml"
- ".github/workflows/build-php-wasm-mysqli-poll-overlay.yml"
- ".github/workflows/run-agent-task.yml"
- "contracts/agent-task-workflow-request.fixture.json"
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
Expand Down Expand Up @@ -95,6 +98,7 @@ on:
- "packages/runtime-core/src/bounded-runtime-plan.ts"
- "tests/bounded-*.test.ts"
- "tests/disposable-mysql-mysqli.integration.test.ts"
- "tests/mysqli-poll.integration.test.ts"

jobs:
contracts:
Expand Down Expand Up @@ -132,6 +136,7 @@ jobs:
- run: npm run test:bounded-recipe-plan-integration
- run: npm run test:recipe-step-continuation
- run: npm run test:disposable-mysql-mysqli-e2e
- run: npm run test:mysqli-poll-integration
- run: npm run test:runtime-sources-playground-integration
- run: npm run test:playground-phpunit-readonly-cache-integration
- run: npm run test:playground-custom-archive-cache
Expand All @@ -152,4 +157,4 @@ jobs:
- uses: actions/checkout@v4
- uses: docker://rhysd/actionlint:1.7.7
with:
args: .github/workflows/agent-task-contracts.yml .github/workflows/run-agent-task.yml
args: .github/workflows/agent-task-contracts.yml .github/workflows/build-php-wasm-mysqli-poll-overlay.yml .github/workflows/run-agent-task.yml
89 changes: 89 additions & 0 deletions .github/workflows/build-php-wasm-mysqli-poll-overlay.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Build PHP-WASM mysqli_poll Overlay

on:
workflow_dispatch:

permissions:
contents: read

jobs:
build-and-verify:
runs-on: ubuntu-latest
timeout-minutes: 300
env:
PLAYGROUND_SHA: 581c7c172428159eb4e6c5309054a568cd39a97a
PLAYGROUND_ARCHIVE_SHA256: b9526f0fb0839cb255d0ae8bb815c548aafe603a3a49b1b8701442a0c1afca8e
PATCH_SHA: d746c930477eae3d726f754753eb92692ad30855
PATCH_SHA256: f42ffb44f21e736c3fbf6e504e01e4a07207f8d764c38ce1f9c81db80d614a04
PHP_VERSION: "8.3.32"
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Reclaim runner disk
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
docker system prune --all --force

- name: Materialize pinned Playground source
working-directory: ${{ runner.temp }}
run: |
curl --fail --location --retry 3 --output playground.tar.gz "https://github.com/WordPress/wordpress-playground/archive/${PLAYGROUND_SHA}.tar.gz"
test "$(shasum -a 256 playground.tar.gz | cut -d ' ' -f 1)" = "${PLAYGROUND_ARCHIVE_SHA256}"
curl --fail --location --retry 3 --output mysqli-poll.patch "https://github.com/WordPress/wordpress-playground/commit/${PATCH_SHA}.patch"
test "$(shasum -a 256 mysqli-poll.patch | cut -d ' ' -f 1)" = "${PATCH_SHA256}"
mkdir wordpress-playground
tar -xzf playground.tar.gz --strip-components=1 -C wordpress-playground
git -C wordpress-playground apply --check --include=packages/php-wasm/compile/php/Dockerfile --include=packages/php-wasm/compile/php/php_wasm.c ../mysqli-poll.patch
git -C wordpress-playground apply --include=packages/php-wasm/compile/php/Dockerfile --include=packages/php-wasm/compile/php/php_wasm.c ../mysqli-poll.patch

- name: Install Playground build dependencies
working-directory: ${{ runner.temp }}/wordpress-playground
run: npm ci

- name: Compile PHP 8.3 Node runtimes
working-directory: ${{ runner.temp }}/wordpress-playground
run: |
node packages/php-wasm/compile/build.js --PLATFORM=node --PHP_VERSION="${PHP_VERSION}"
node packages/php-wasm/compile/build.js --PLATFORM=node --PHP_VERSION="${PHP_VERSION}" --WITH_JSPI=yes
npx nx build php-wasm-node-8-3

- name: Pack overlay and record provenance
working-directory: ${{ runner.temp }}/wordpress-playground
run: |
mkdir -p "${GITHUB_WORKSPACE}/artifacts"
npm pack ./dist/packages/php-wasm/node-builds/8-3 --pack-destination "${GITHUB_WORKSPACE}/artifacts"
ARTIFACT_PATH="$(find "${GITHUB_WORKSPACE}/artifacts" -name '*.tgz' -type f -print -quit)"
ARTIFACT_SHA256="$(shasum -a 256 "${ARTIFACT_PATH}" | cut -d ' ' -f 1)"
node -e 'const fs = require("node:fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ schema: "wp-codebox/php-wasm-overlay-provenance/v1", upstream: { repository: "WordPress/wordpress-playground", revision: process.env.PLAYGROUND_SHA }, patch: { revision: process.env.PATCH_SHA, sha256: process.env.PATCH_SHA256, files: ["packages/php-wasm/compile/php/Dockerfile", "packages/php-wasm/compile/php/php_wasm.c"] }, artifact: { file: require("node:path").basename(process.argv[2]), sha256: process.argv[3] } }, null, 2) + "\n")' "${GITHUB_WORKSPACE}/artifacts/provenance.json" "${ARTIFACT_PATH}" "${ARTIFACT_SHA256}"

- name: Reclaim build disk
run: |
docker system prune --all --force
rm -rf "${RUNNER_TEMP}/wordpress-playground" "${RUNNER_TEMP}/playground.tar.gz" "${RUNNER_TEMP}/mysqli-poll.patch"

- name: Install WP Codebox and overlay
run: |
npm ci
npm install --no-save --ignore-scripts "$(find "${GITHUB_WORKSPACE}/artifacts" -name '*.tgz' -type f -print -quit)"
npm run build

- name: Verify mysqli_poll against MariaDB
env:
PLAYGROUND_NO_JSPI_RESPAWN: "1"
WP_CODEBOX_PHP_WASM_MODE: asyncify
run: npm run test:mysqli-poll-integration

- name: Upload immutable overlay
if: always()
uses: actions/upload-artifact@v4
with:
name: php-wasm-node-8-3-mysqli-poll-${{ github.sha }}
path: |
artifacts/*.tgz
artifacts/provenance.json
if-no-files-found: error
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ node_modules/
vendor/
dist/
artifacts/
runtime-overlays/*
!runtime-overlays/php-wasm-node-8-3-3.1.46.tgz
!runtime-overlays/provenance.json
scripts/uploads/
*.tsbuildinfo
.DS_Store
Expand Down
5 changes: 3 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"npm-shrinkwrap.json",
"scripts/apply-development-patches.mjs",
"patches",
"runtime-overlays/php-wasm-node-8-3-3.1.46.tgz",
"runtime-overlays/provenance.json",
"README.md",
"LICENSE"
],
Expand Down Expand Up @@ -288,6 +290,7 @@
"test:managed-database-diagnostics": "tsx tests/managed-database-diagnostics.test.ts",
"test:runtime-services-lifecycle": "tsx tests/runtime-services-lifecycle.test.ts",
"test:native-mariadb-runtime-service-integration": "tsx tests/native-mariadb-runtime-service.integration.test.ts",
"test:mysqli-poll-integration": "tsx tests/mysqli-poll.integration.test.ts",
"test:disposable-mysql-mysqli-e2e": "tsx tests/disposable-mysql-mysqli.integration.test.ts",
"test:runtime-contract-manifest": "tsx tests/runtime-contract-manifest.test.ts",
"test:runtime-contract-package-exports": "tsx tests/runtime-contract-package-exports.test.ts",
Expand Down Expand Up @@ -338,6 +341,7 @@
"@automattic/wp-codebox-cli": "file:packages/cli",
"@automattic/wp-codebox-core": "file:packages/runtime-core",
"@automattic/wp-codebox-playground": "file:packages/runtime-playground",
"@php-wasm/node-8-3": "file:runtime-overlays/php-wasm-node-8-3-3.1.46.tgz",
"@php-wasm/node-8-4": "3.1.46",
"@php-wasm/universal": "3.1.46",
"@types/pngjs": "^6.0.5",
Expand Down Expand Up @@ -459,8 +463,8 @@
"on-finished": "^2.4.1",
"once": "^1.4.0",
"pako": "^1.0.11",
"patch-package": "^8.0.1",
"parseurl": "^1.3.3",
"patch-package": "^8.0.1",
"path-expression-matcher": "^1.5.0",
"path-to-regexp": "^0.1.13",
"pify": "^4.0.1",
Expand Down
9 changes: 9 additions & 0 deletions patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ the released CLI's log-only behavior.
Without this narrow terminalization, a poisoned PHP-WASM worker remains alive
and leaves the parent request pending until the recipe timeout. Remove the patch
after the Playground CLI ships the same worker terminalization behavior.

The manual `build-php-wasm-mysqli-poll-overlay.yml` workflow carries the C-level
`select()` wrapper and Asyncify imports from WordPress Playground PR
[#4170](https://github.com/WordPress/wordpress-playground/pull/4170). It
checksum-verifies the immutable patch and Playground revision
`581c7c172428159eb4e6c5309054a568cd39a97a`, rebuilds the PHP 8.3.32 Node
package, and verifies `mysqli_poll()` against MariaDB before uploading the
package and provenance. Remove the patch, vendored package, and workflow after
a published Playground package contains the same fix.
Binary file added runtime-overlays/php-wasm-node-8-3-3.1.46.tgz
Binary file not shown.
19 changes: 19 additions & 0 deletions runtime-overlays/provenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"schema": "wp-codebox/php-wasm-overlay-provenance/v1",
"upstream": {
"repository": "WordPress/wordpress-playground",
"revision": "581c7c172428159eb4e6c5309054a568cd39a97a"
},
"patch": {
"revision": "d746c930477eae3d726f754753eb92692ad30855",
"sha256": "f42ffb44f21e736c3fbf6e504e01e4a07207f8d764c38ce1f9c81db80d614a04",
"files": [
"packages/php-wasm/compile/php/Dockerfile",
"packages/php-wasm/compile/php/php_wasm.c"
]
},
"artifact": {
"file": "php-wasm-node-8-3-3.1.46.tgz",
"sha256": "109c079c958e1ab35ebcd098bdd895b080e2c5405b147f93ecb04e768c723553"
}
}
88 changes: 88 additions & 0 deletions tests/mysqli-poll.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import assert from "node:assert/strict"
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runRecipe } from "../packages/cli/src/commands/recipe-run.ts"

const directory = await mkdtemp(join(tmpdir(), "wp-codebox-mysqli-poll-"))
try {
const recipePath = join(directory, "recipe.json")
const code = `
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$connect = static function (): mysqli {
$db = mysqli_init();
mysqli_real_connect($db, getenv('DB_HOST'), getenv('DB_USER'), getenv('DB_PASSWORD'), getenv('DB_NAME'), (int) getenv('DB_PORT'));
return $db;
};
$first = $connect();
$second = $connect();
$first->query('CREATE TABLE poll_lock (id INT PRIMARY KEY) ENGINE=InnoDB');
$first->query('INSERT INTO poll_lock VALUES (1)');
$first->query('START TRANSACTION');
$first->query('SELECT id FROM poll_lock WHERE id = 1 FOR UPDATE');
$second->query('SELECT id FROM poll_lock WHERE id = 1 FOR UPDATE', MYSQLI_ASYNC);
$read = array($second);
$error = array();
$reject = array();
$started = microtime(true);
$ready = mysqli_poll($read, $error, $reject, 0, 100000);
$elapsed_ms = (microtime(true) - $started) * 1000;
$timeout_set_counts = array(count($read), count($error), count($reject));
$first->query('ROLLBACK');
$cleanup_ready = false;
for ($attempt = 0; $attempt < 20; $attempt++) {
$read = array($second);
$error = array();
$reject = array();
if (mysqli_poll($read, $error, $reject, 0, 100000) > 0) {
$cleanup_ready = true;
break;
}
}
if ($cleanup_ready) {
$second->reap_async_query();
}
$first->query('DROP TABLE poll_lock');
echo json_encode(array(
'ready' => $ready,
'elapsed_ms' => $elapsed_ms,
'timeout_set_counts' => $timeout_set_counts,
'cleanup_ready' => $cleanup_ready,
));
`
await writeFile(recipePath, JSON.stringify({
schema: "wp-codebox/workspace-recipe/v1",
runtime: { phpVersion: "8.3" },
inputs: {
services: [{
id: "mariadb",
kind: "mysql",
configuration: { engine: "mariadb" },
outputs: { host: "DB_HOST", port: "DB_PORT", username: "DB_USER", password: "DB_PASSWORD", database: "DB_NAME" },
}],
},
workflow: { steps: [{ command: "wordpress.run-php", args: [`code=${code}`] }] },
}))

const result = await runRecipe({
recipePath,
previewHoldBlocking: false,
previewLeaseRequested: false,
previewLeaseChild: false,
timeoutMs: 180_000,
json: true,
summary: false,
dryRun: false,
externalServiceWritesApproved: false,
})
assert.equal(result.success, true, JSON.stringify(result))
const output = JSON.parse(result.executions.at(-1)?.stdout.trim() ?? "{}")
assert.equal(output.ready, 0)
assert.ok(output.elapsed_ms >= 50, `mysqli_poll returned too early after ${output.elapsed_ms}ms`)
assert.ok(output.elapsed_ms < 2_000, `mysqli_poll exceeded its timeout: ${output.elapsed_ms}ms`)
assert.deepEqual(output.timeout_set_counts, [0, 0, 0])
assert.equal(output.cleanup_ready, true)
console.log("mysqli_poll MariaDB integration passed")
} finally {
await rm(directory, { recursive: true, force: true })
}
Loading