Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
109 changes: 75 additions & 34 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,44 +76,85 @@ jobs:
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh

EGG_TS_ENABLE=false npx eggctl start --port=7001 --env=unittest --daemon
HEALTH_URL="http://127.0.0.1:7001/"
START_TIME=$(date +%s)
TIMEOUT=120
STATUS=""
READY=0

echo "Waiting for application to become healthy at ${HEALTH_URL} (timeout: ${TIMEOUT}s)..."
while true; do
# Capture response body and status code
STATUS=$(curl -s -o /tmp/cnpmcore-health-response -w "%{http_code}" "${HEALTH_URL}" || echo "000")
echo "Health check attempt at $(date): status=${STATUS}"

if [ "${STATUS}" = "200" ]; then
echo "Health check succeeded with status 200"
READY=1
break
fi

NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if [ "${ELAPSED}" -ge "${TIMEOUT}" ]; then
echo "Health check timed out after ${ELAPSED}s with last status ${STATUS}"
break
fi

sleep 5
done

npx eggctl stop

if [ "${READY}" != "1" ]; then
echo "Health check failed; last HTTP status: ${STATUS}"
echo "Last health endpoint response body (if any):"
cat /tmp/cnpmcore-health-response 2>/dev/null || echo "<no response body captured>"

if bash ../wait-health.sh "http://127.0.0.1:7001/" /tmp/cnpmcore-health-response 120 5; then
npx eggctl stop
else
npx eggctl stop || true
echo "Recent error logs (if any):"
cat logs/cnpmcore/common-error.log 2>/dev/null || true
exit 1
fi
- name: cnpmcore-snapshot
node-version: 24
command: |
# V8 startup snapshot regression: build a snapshot of cnpmcore, restore
# it via `egg-scripts start --snapshot-blob`, and assert it serves
# `/-/ping` with HTTP 200. Restoring a V8 snapshot requires Node.js >= 24.
export EGG_TS_ENABLE=false
# Snapshot under the production env: it loads only config.default (DB
# comes from CNPMCORE_DATABASE_* env vars) and does NOT pull in test-only
# plugins like @eggjs/mock, so the snapshot stays lean and production-like.
# (unittest would bake the mock plugin + config.unittest into the blob.)
export EGG_SERVER_ENV=prod
export CNPMCORE_DATABASE_NAME=cnpmcore
# cnpmcore's NFSClientAdapter throws under prod when the store is local fs
# unless this is set. The snapshot e2e only needs a working /-/ping, so allow
# the local-fs store instead of wiring up OSS/S3.
export CNPMCORE_FORCE_LOCAL_FS=true

npm install --legacy-peer-deps

# Compile to JS and overlay onto source so the egg loader and tegg module
# scanner resolve .js at the expected paths (same approach as the cnpmcore
# job); the snapshot is built from the compiled app.
npm run tsc:prod
cp -r dist/* .
rm -rf dist
# Remove the now-redundant .ts sources so the bundler scans only .js. With
# both present, tegg's globby picks up the .ts AND .js of the same class as
# two modules (the "duplicate proto" hazard): a decorated class is bundled
# twice and only one copy's filePath is corrected, which breaks AOP advice
# resolution on restore (e.g. "Aop Advice(AsyncTimer) not found in loadUnits").
find app config -name '*.ts' ! -name '*.d.ts' -delete

# Under EGG_SERVER_ENV=prod the DB name comes from CNPMCORE_DATABASE_NAME
# (set to cnpmcore above), so prepare that database.
echo "Preparing database..."
mysql -h 127.0.0.1 -u root -e "CREATE DATABASE IF NOT EXISTS cnpmcore"
bash ./prepare-database-mysql.sh

# Build the V8 startup snapshot. Building works on Node.js >= 22 (CI runs
# on 24 here). leoric is auto-externalized (native optional DB drivers);
# @cnpmjs/packument / koa-onerror / undici / urllib are kept external. The
# network stack stays lazy-external (no manual stubs) so the blob is
# restorable on Node.js >= 24.
npx egg-bin snapshot build \
--output ./dist-bundle \
--force-external @cnpmjs/packument \
--force-external koa-onerror \
--force-external undici \
--force-external urllib

# Restore the snapshot and verify it serves traffic. --no-sourcemap keeps
# `--import source-map-support` out of the `node --snapshot-blob` process
# (cnpmcore sets egg.typescript, which would otherwise auto-enable it).
npx egg-scripts start \
--snapshot-blob ./dist-bundle/snapshot.blob \
--port 7001 \
--no-sourcemap \
--daemon \
--title=egg-server-cnpmcore-snapshot

if bash ../wait-health.sh "http://127.0.0.1:7001/-/ping" /tmp/cnpmcore-snapshot-health-response 120 3; then
npx egg-scripts stop --title=egg-server-cnpmcore-snapshot
else
npx egg-scripts stop --title=egg-server-cnpmcore-snapshot || true
echo "Recent snapshot error logs (if any):"
cat dist-bundle/logs/cnpmcore/common-error.log 2>/dev/null || true
cat "${HOME}/logs/master-stderr.log" 2>/dev/null || true
exit 1
fi
- name: examples
node-version: 24
command: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ tegg/plugin/tegg/test/fixtures/apps/**/*.js
*.tgz

ecosystem-ci/cnpmcore
ecosystem-ci/cnpmcore-snapshot
ecosystem-ci/examples
pnpm-lock.yaml
.utoo.toml
Expand Down
9 changes: 6 additions & 3 deletions ecosystem-ci/patch-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ async function patchPackageJSON(filePath: string, overrides: Record<string, stri
fs.writeFileSync(filePath, packageJsonString);
}

async function patchCnpmcore(overrides: Record<string, string>): Promise<void> {
const packageJsonPath = join(projectDir, 'cnpmcore', 'package.json');
async function patchProjectRoot(projectName: string, overrides: Record<string, string>): Promise<void> {
const packageJsonPath = join(projectDir, projectName, 'package.json');
await patchPackageJSON(packageJsonPath, overrides);
}

Expand All @@ -111,7 +111,10 @@ async function main(): Promise<void> {

switch (project) {
case 'cnpmcore':
await patchCnpmcore(overrides);
// cnpmcore-snapshot is a second checkout of the same cnpmcore repo used by
// the V8 snapshot e2e job; it patches its own package.json the same way.
case 'cnpmcore-snapshot':
await patchProjectRoot(project, overrides);
break;
case 'examples':
await patchExamples(overrides);
Expand Down
5 changes: 5 additions & 0 deletions ecosystem-ci/repo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"branch": "master",
"hash": "98463c33188bbd32513a74e6c3a2c4cc559ef5da"
},
"cnpmcore-snapshot": {
"repository": "https://github.com/cnpm/cnpmcore.git",
"branch": "master",
"hash": "98463c33188bbd32513a74e6c3a2c4cc559ef5da"
},
"examples": {
"repository": "https://github.com/eggjs/examples.git",
"branch": "master",
Expand Down
43 changes: 43 additions & 0 deletions ecosystem-ci/wait-health.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Poll an HTTP endpoint until it returns 200, or time out.
#
# Usage: wait-health.sh <url> <response-file> [timeout-seconds] [sleep-seconds]
# Exits 0 on the first HTTP 200; exits 1 on timeout (dumping the last response body).
#
# Shared by the cnpmcore and cnpmcore-snapshot e2e jobs in
# .github/workflows/e2e-test.yml. Deliberately does not `set -e` so the
# curl-poll loop controls flow; callers wrap it in `if wait-health.sh ...; then`.
set -uo pipefail

if [ "$#" -lt 2 ]; then
echo "Usage: $0 <url> <response-file> [timeout-seconds] [sleep-seconds]" >&2
exit 1
fi

URL="$1"
RESPONSE_FILE="$2"
Comment on lines +17 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since set -u is enabled, running this script with fewer than 2 arguments will cause it to crash immediately with an unbound variable error. Adding an explicit argument count check provides a much friendlier and clearer usage error message.

Suggested change
URL="$1"
RESPONSE_FILE="$2"
if [ $# -lt 2 ]; then
echo "Usage: $0 <url> <response-file> [timeout-seconds] [sleep-seconds]" >&2
exit 1
fi
URL="$1"
RESPONSE_FILE="$2"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a6417bb — added an explicit [ "$#" -lt 2 ] usage check with a friendly message before the set -u positional reads.

TIMEOUT="${3:-120}"
SLEEP="${4:-5}"

START_TIME=$(date +%s)
echo "Waiting for ${URL} to return HTTP 200 (timeout: ${TIMEOUT}s)..."
while true; do
STATUS=$(curl -s -o "${RESPONSE_FILE}" -w "%{http_code}" "${URL}" || echo "000")
echo "Health check at $(date): status=${STATUS}"

if [ "${STATUS}" = "200" ]; then
echo "Health check succeeded with status 200"
exit 0
fi

NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if [ "${ELAPSED}" -ge "${TIMEOUT}" ]; then
echo "Health check timed out after ${ELAPSED}s with last status ${STATUS}"
echo "Last response body (if any):"
cat "${RESPONSE_FILE}" 2>/dev/null || echo "<no response body captured>"
exit 1
fi

sleep "${SLEEP}"
done
2 changes: 2 additions & 0 deletions site/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] {
{ text: 'Loader', link: 'loader' },
{ text: 'Plugin Development', link: 'plugin' },
{ text: 'Framework Development', link: 'framework' },
{ text: 'V8 Startup Snapshot', link: 'snapshot' },
],
},
];
Expand Down Expand Up @@ -404,6 +405,7 @@ function sidebarAdvancedZhCN(): DefaultTheme.SidebarItem[] {
{ text: 'View 插件开发', link: 'view-plugin' },
{ text: '升级你的生命周期事件函数', link: 'loader-update' },
{ text: '对象生命周期', link: 'lifecycle' },
{ text: 'V8 启动快照', link: 'snapshot' },
],
},
];
Expand Down
Loading
Loading