Skip to content

Commit a025213

Browse files
fengmk2claudeCopilot
authored
test: add e2e tests (#5739)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added automated end-to-end testing workflow for multiple ecosystem projects. * Introduced ecosystem CI tooling to clone, sync, and patch dependent projects for local testing. * **Chores** * Exposed README in packaged files and updated dev dependency declarations. * Updated ignore rules to exclude packaged artifacts and ecosystem directories. * Simplified plugin dependencies and adjusted build config externals. * **Tests** * Skip one timing-sensitive test on Windows to improve CI stability. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: MK (fengmk2) <fengmk2@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1cedb32 commit a025213

File tree

13 files changed

+359
-12
lines changed

13 files changed

+359
-12
lines changed

.github/actions/clone/action.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: 'Clone Repositories'
2+
description: 'Clone self and upstream repositories'
3+
4+
inputs:
5+
ecosystem-ci-project:
6+
description: 'The ecosystem ci project to clone'
7+
required: false
8+
default: ''
9+
10+
runs:
11+
using: 'composite'
12+
steps:
13+
- name: Output ecosystem ci project hash
14+
shell: bash
15+
id: ecosystem-ci-project-hash
16+
if: ${{ inputs.ecosystem-ci-project != '' }}
17+
run: |
18+
node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('./ecosystem-ci/repo.json', 'utf8')); const project = data['${{ inputs.ecosystem-ci-project }}']; console.log('ECOSYSTEM_CI_PROJECT_HASH=' + project.hash);" >> $GITHUB_OUTPUT
19+
node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('./ecosystem-ci/repo.json', 'utf8')); const project = data['${{ inputs.ecosystem-ci-project }}']; console.log('ECOSYSTEM_CI_PROJECT_REPOSITORY=' + project.repository.replace('https://github.com/', '').replace('.git', ''));" >> $GITHUB_OUTPUT
20+
21+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
22+
if: ${{ inputs.ecosystem-ci-project != '' }}
23+
with:
24+
repository: ${{ steps.ecosystem-ci-project-hash.outputs.ECOSYSTEM_CI_PROJECT_REPOSITORY }}
25+
path: ecosystem-ci/${{ inputs.ecosystem-ci-project }}
26+
ref: ${{ steps.ecosystem-ci-project-hash.outputs.ECOSYSTEM_CI_PROJECT_HASH }}

.github/workflows/e2e-test.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: E2E Test
2+
3+
on:
4+
push:
5+
branches:
6+
- next
7+
paths-ignore:
8+
- '**/*.md'
9+
pull_request:
10+
branches:
11+
- next
12+
paths-ignore:
13+
- '**/*.md'
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
17+
cancel-in-progress: true
18+
19+
defaults:
20+
run:
21+
shell: bash
22+
23+
jobs:
24+
e2e-test:
25+
name: ${{ matrix.project.name }} E2E test
26+
permissions:
27+
contents: read
28+
packages: read
29+
runs-on: ubuntu-latest
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
project:
34+
- name: cnpmcore
35+
node-version: 24
36+
command: |
37+
npm install
38+
npm run lint -- --quiet
39+
npm run typecheck
40+
npm run build
41+
npm run prepublishOnly
42+
- name: examples
43+
node-version: 24
44+
command: |
45+
# examples/helloworld https://github.com/eggjs/examples/blob/master/helloworld/package.json
46+
cd helloworld
47+
npm install
48+
npm run lint
49+
npm run test
50+
npm run prepublishOnly
51+
cd ..
52+
53+
# examples/hello-tegg https://github.com/eggjs/examples/blob/master/hello-tegg/package.json
54+
cd hello-tegg
55+
npm install
56+
npm run lint
57+
npm run test
58+
npm run prepublishOnly
59+
steps:
60+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
61+
- uses: ./.github/actions/clone
62+
with:
63+
ecosystem-ci-project: ${{ matrix.project.name }}
64+
65+
- name: Install pnpm
66+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
67+
68+
- name: Set up Node.js
69+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
70+
with:
71+
node-version: ${{ matrix.project.node-version }}
72+
cache: 'pnpm'
73+
74+
- name: Install dependencies
75+
run: pnpm install --frozen-lockfile
76+
77+
- name: Build all packages
78+
run: pnpm build
79+
80+
- name: Pack packages into tgz
81+
run: |
82+
pnpm -r pack
83+
84+
- name: Override dependencies from tgz in ${{ matrix.project.name }}
85+
working-directory: ecosystem-ci/${{ matrix.project.name }}
86+
run: |
87+
node ../patch-project.ts ${{ matrix.project.name }}
88+
89+
- name: Run e2e test commands in ${{ matrix.project.name }}
90+
working-directory: ecosystem-ci/${{ matrix.project.name }}
91+
run: ${{ matrix.project.command }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,5 @@ tegg/plugin/tegg/test/fixtures/apps/**/*.js
102102
!tegg/plugin/tegg/test/fixtures/**/node_modules
103103
!tegg/plugin/config/test/fixtures/**/node_modules
104104

105-
*.tsbuildinfo
105+
*.tsbuildinfo
106+
*.tgz

ecosystem-ci/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cnpmcore
2+
examples

ecosystem-ci/clone.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { execSync } from 'node:child_process';
2+
import { existsSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
5+
import repos from './repo.json' with { type: 'json' };
6+
7+
const cwd = import.meta.dirname;
8+
9+
function exec(cmd: string, execCwd: string = cwd): string {
10+
return execSync(cmd, { cwd: execCwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
11+
}
12+
13+
function getRemoteUrl(dir: string): string | null {
14+
try {
15+
return exec('git remote get-url origin', dir);
16+
} catch {
17+
return null;
18+
}
19+
}
20+
21+
function normalizeGitUrl(url: string): string {
22+
// Convert git@github.com:owner/repo.git to github.com/owner/repo
23+
// Convert https://github.com/owner/repo.git to github.com/owner/repo
24+
return url
25+
.replace(/^git@([^:]+):/, '$1/')
26+
.replace(/^https?:\/\//, '')
27+
.replace(/\.git$/, '');
28+
}
29+
30+
function isSameRepo(url1: string, url2: string): boolean {
31+
return normalizeGitUrl(url1) === normalizeGitUrl(url2);
32+
}
33+
34+
function getCurrentHash(dir: string): string | null {
35+
try {
36+
return exec('git rev-parse HEAD', dir);
37+
} catch {
38+
return null;
39+
}
40+
}
41+
42+
function cloneRepo(repoUrl: string, branch: string, targetDir: string): void {
43+
console.info(`Cloning ${repoUrl} (branch: ${branch})...`);
44+
exec(`git clone --branch ${branch} ${repoUrl} ${targetDir}`);
45+
}
46+
47+
function checkoutHash(dir: string, hash: string): void {
48+
console.info(`Checking out ${hash}...`);
49+
exec(`git fetch origin`, dir);
50+
exec(`git checkout ${hash}`, dir);
51+
}
52+
53+
for (const [repoName, repo] of Object.entries(repos)) {
54+
const targetDir = join(cwd, repoName);
55+
56+
if (existsSync(targetDir)) {
57+
console.info(`\nDirectory ${repoName} exists, validating...`);
58+
59+
const remoteUrl = getRemoteUrl(targetDir);
60+
if (!remoteUrl) {
61+
console.error(` ✗ ${repoName} is not a git repository`);
62+
continue;
63+
}
64+
65+
if (!isSameRepo(remoteUrl, repo.repository)) {
66+
console.error(` ✗ Remote mismatch: expected ${repo.repository}, got ${remoteUrl}`);
67+
continue;
68+
}
69+
70+
console.info(` ✓ Remote matches`);
71+
72+
const currentHash = getCurrentHash(targetDir);
73+
if (currentHash === repo.hash) {
74+
console.info(` ✓ Already at correct commit ${repo.hash.slice(0, 7)}`);
75+
} else {
76+
console.info(` → Current: ${currentHash?.slice(0, 7)}, expected: ${repo.hash.slice(0, 7)}`);
77+
checkoutHash(targetDir, repo.hash);
78+
console.info(` ✓ Checked out ${repo.hash.slice(0, 7)}`);
79+
}
80+
} else {
81+
cloneRepo(repo.repository, repo.branch, targetDir);
82+
checkoutHash(targetDir, repo.hash);
83+
console.info(`✓ Cloned and checked out ${repo.hash.slice(0, 7)}`);
84+
}
85+
}
86+
87+
console.info('\nDone!');

ecosystem-ci/patch-project.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import fs from 'node:fs';
2+
import { glob } from 'node:fs/promises';
3+
import { dirname, join, relative } from 'node:path';
4+
5+
import yaml from 'js-yaml';
6+
7+
import repos from './repo.json' with { type: 'json' };
8+
9+
const projectDir = import.meta.dirname;
10+
const rootDir = join(projectDir, '..');
11+
const tgzPath = rootDir;
12+
13+
const projects = Object.keys(repos);
14+
15+
const project = process.argv[2];
16+
17+
if (!projects.includes(project)) {
18+
console.error(`Project ${project} is not defined in repo.json. Valid projects: ${projects.join(', ')}`);
19+
process.exit(1);
20+
}
21+
22+
// Read pnpm-workspace.yaml to get workspace patterns
23+
const workspaceConfig = yaml.load(fs.readFileSync(join(rootDir, 'pnpm-workspace.yaml'), 'utf8')) as {
24+
packages: string[];
25+
};
26+
27+
// Use glob to find all package directories dynamically
28+
async function discoverPackages(): Promise<[string, string][]> {
29+
const packages: [string, string][] = [];
30+
31+
for (const pattern of workspaceConfig.packages) {
32+
// Convert pnpm patterns (e.g., 'packages/*') to glob patterns for package.json
33+
const globPattern = `${pattern}/package.json`;
34+
35+
for await (const entry of glob(globPattern, { cwd: rootDir })) {
36+
const pkgJsonPath = join(rootDir, entry);
37+
try {
38+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
39+
40+
// Skip private packages and packages without names
41+
if (pkgJson.private || !pkgJson.name) continue;
42+
43+
const relativePath = dirname(relative(rootDir, pkgJsonPath));
44+
packages.push([pkgJson.name, relativePath]);
45+
} catch {
46+
// Skip if package.json is invalid or cannot be read
47+
console.warn(`Warning: Could not read ${pkgJsonPath}`);
48+
}
49+
}
50+
}
51+
52+
return packages;
53+
}
54+
55+
async function buildOverrides(): Promise<Record<string, string>> {
56+
const packages = await discoverPackages();
57+
const overrides: Record<string, string> = {};
58+
59+
for (const [name, path] of packages) {
60+
const version = JSON.parse(fs.readFileSync(join(tgzPath, path, 'package.json'), 'utf8')).version;
61+
const filename = `${name.replace('@', '').replace('/', '-')}-${version}.tgz`;
62+
overrides[name] = `file:${tgzPath}/${filename}`;
63+
}
64+
65+
return overrides;
66+
}
67+
68+
async function patchPackageJSON(filePath: string, overrides: Record<string, string>): Promise<void> {
69+
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
70+
// Add overrides with tgz files
71+
packageJson.overrides = {
72+
...packageJson.overrides,
73+
...overrides,
74+
};
75+
76+
for (const name in packageJson.dependencies) {
77+
const override = overrides[name];
78+
if (override) {
79+
packageJson.dependencies[name] = override;
80+
}
81+
}
82+
for (const name in packageJson.devDependencies) {
83+
const override = overrides[name];
84+
if (override) {
85+
packageJson.devDependencies[name] = override;
86+
}
87+
}
88+
89+
const packageJsonString = JSON.stringify(packageJson, null, 2) + '\n';
90+
console.log(packageJsonString);
91+
fs.writeFileSync(filePath, packageJsonString);
92+
}
93+
94+
async function patchCnpmcore(overrides: Record<string, string>): Promise<void> {
95+
const packageJsonPath = join(projectDir, 'cnpmcore', 'package.json');
96+
await patchPackageJSON(packageJsonPath, overrides);
97+
}
98+
99+
async function patchExamples(overrides: Record<string, string>): Promise<void> {
100+
// https://github.com/eggjs/examples/tree/master/hello-tegg
101+
let packageJsonPath = join(projectDir, 'examples', 'hello-tegg', 'package.json');
102+
await patchPackageJSON(packageJsonPath, overrides);
103+
104+
// https://github.com/eggjs/examples/blob/master/helloworld/package.json
105+
packageJsonPath = join(projectDir, 'examples', 'helloworld', 'package.json');
106+
await patchPackageJSON(packageJsonPath, overrides);
107+
}
108+
109+
async function main(): Promise<void> {
110+
const overrides = await buildOverrides();
111+
112+
switch (project) {
113+
case 'cnpmcore':
114+
await patchCnpmcore(overrides);
115+
break;
116+
case 'examples':
117+
await patchExamples(overrides);
118+
break;
119+
default:
120+
console.error(`Project ${project} is not supported`);
121+
process.exit(1);
122+
}
123+
}
124+
125+
main();

ecosystem-ci/repo.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"cnpmcore": {
3+
"repository": "https://github.com/cnpm/cnpmcore.git",
4+
"branch": "master",
5+
"hash": "8a1b79f3e0ec1aa1a9dcd37d059846bc4f4219f8"
6+
},
7+
"examples": {
8+
"repository": "https://github.com/eggjs/examples.git",
9+
"branch": "master",
10+
"hash": "9191a92e5ec0712a2e4864e999b7db1874fe8ca3"
11+
}
12+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"type": "git",
99
"url": "git+https://github.com/eggjs/egg.git"
1010
},
11+
"files": [
12+
"README.md"
13+
],
1114
"type": "module",
1215
"scripts": {
1316
"clean-dist": "pnpm -r --parallel exec rimraf dist",
@@ -45,6 +48,7 @@
4548
"devDependencies": {
4649
"@eggjs/bin": "workspace:*",
4750
"@eggjs/tsconfig": "workspace:*",
51+
"@types/js-yaml": "catalog:",
4852
"@types/node": "catalog:",
4953
"@typescript/native-preview": "catalog:",
5054
"@vitest/coverage-v8": "catalog:",

plugins/schedule/test/immediate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe.skipIf(process.platform === 'win32')('cluster - immediate', () => {
2525
});
2626
});
2727

28-
describe('cluster - immediate-onlyonce', () => {
28+
describe.skipIf(process.platform === 'win32')('cluster - immediate-onlyonce', () => {
2929
let app: MockApplication;
3030
beforeAll(async () => {
3131
app = mm.cluster({

0 commit comments

Comments
 (0)