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
137 changes: 92 additions & 45 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,46 @@ name: CI

on:
push:
paths-ignore:
- '**/*.md'
branches: [next]
pull_request:
types: [opened, synchronize]
paths-ignore:
- '**/*.md'
types: [opened, synchronize, reopened]
branches: [next]
merge_group:
workflow_dispatch:

permissions:
contents: read
pull-requests: read
id-token: write
actions: write

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
plan:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
tests: ${{ steps.plan.outputs.tests }}
profile: ${{ steps.plan.outputs.profile }}
coverage-node: ${{ steps.node.outputs.node-version }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
id: node
with:
node-version: '24'
check-latest: true
- name: Check CI planning and coverage guards
run: node --test scripts/test/ci.test.js
- name: Plan tests
id: plan
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/ci-plan.js

typecheck:
runs-on: ubuntu-latest

Expand Down Expand Up @@ -65,29 +90,20 @@ jobs:
run: ut run site:build

test:
needs: plan
if: needs.plan.outputs.tests == 'true'
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
node: ['22', '24', '26']
coverage: [false]
# Replace (not duplicate) the ubuntu/24 leg with a coverage-enabled one.
# `include` cannot overwrite an existing matrix dimension, so the base
# coverage:false combo must be excluded first or ubuntu/24 would run twice.
exclude:
- os: 'ubuntu-latest'
node: '24'
coverage: false
include:
- os: 'ubuntu-latest'
node: '24'
coverage: true
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}

name: Test (${{ matrix.os }}, ${{ matrix.node }})
name: Test (${{ matrix.os }}, ${{ matrix.node }}, ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
runs-on: ${{ matrix.os }}
env:
CI_TEST_REPORT: '1'
CI_COVERAGE_REPORT: ${{ matrix.coverage && '1' || '0' }}

concurrency:
group: test-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-(${{ matrix.os }}, ${{ matrix.node }})
group: test-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-${{ matrix.os }}-${{ matrix.node }}-${{ matrix.shardIndex }}
cancel-in-progress: true

steps:
Expand Down Expand Up @@ -190,7 +206,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ matrix.node }}
node-version: ${{ matrix.coverage && needs.plan.outputs.coverage-node || matrix.node }}

- name: Cache Node compile cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
Expand All @@ -199,7 +215,6 @@ jobs:
key: node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('pnpm-workspace.yaml', '**/package.json') }}
restore-keys: |
node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-
node-compile-cache-${{ runner.os }}-

- name: Enable Node compile cache
# runner.* is not available in job-level env, so export NODE_COMPILE_CACHE
Expand All @@ -222,36 +237,60 @@ jobs:

- name: Run tests (with coverage)
if: ${{ matrix.coverage }}
run: ut run ci
run: ut run ci -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

- name: Run tests
if: ${{ !matrix.coverage }}
run: ut run test

# Summarize how parallel the full-isolate-off suite actually ran. The gating
# test run above (`ut run ci` on the coverage leg, `ut run test` on the rest)
# emits Vitest JSON (vitest.config.ts, CI only); this step turns it into
# avg/peak concurrency + efficiency metrics in the job summary. It is
# informational only (always() so failures still surface metrics) and never
# changes the gate — the test run above already set the job's pass/fail.
run: ut run test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

- name: Report parallelism metrics
if: always()
# Call node directly: `ut run <script> -- ...` re-serializes forwarded args into
# a shell string without re-quoting, so the parentheses in --name break it.
run: node scripts/ci-test-benchmark.js --report-only --vitest-json benchmark/ci-test/ci-run/vitest-results.json --output-dir benchmark/ci-test/ci-run --name "Parallel test metrics (${{ matrix.os }}, Node ${{ matrix.node }}, isolate=off)"
run: node scripts/ci-test-benchmark.js --report-only --vitest-json benchmark/ci-test/ci-run/vitest-results.json --output-dir benchmark/ci-test/ci-run --name "Parallel test metrics (${{ matrix.os }}, Node ${{ matrix.node }}, shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})"

- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ci-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.shardIndex }}-of-${{ matrix.shardTotal }}
path: benchmark/ci-test/ci-run/
overwrite: true
if-no-files-found: error
retention-days: 7

- name: Run example tests
if: ${{ matrix.os != 'windows-latest' }}
run: |
ut run example:test:all
if: ${{ matrix.os != 'windows-latest' && matrix.shardIndex == 1 }}
run: ut run example:test:all

coverage:
needs: [plan, test]
if: needs.plan.outputs.tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: utooland/setup-utoo@e7aa4d726a17f79f68aed736476ea5bd68f8ba52 # main
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ needs.plan.outputs.coverage-node }}
- run: ut install --from pnpm
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ci-results-ubuntu-latest-node24-*
path: benchmark/ci-test/shards
- name: Check coverage inventory
run: node scripts/ci-coverage.js benchmark/ci-test/shards benchmark/ci-test/blobs
- name: Merge coverage
run: node node_modules/vitest/vitest.mjs --merge-reports=benchmark/ci-test/blobs --coverage
- name: Code Coverage
if: ${{ matrix.coverage }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
with:
use_oidc: true
fail_ci_if_error: true

test-egg-bin:
needs: plan
if: needs.plan.outputs.tests == 'true'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -293,7 +332,6 @@ jobs:
key: node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('pnpm-workspace.yaml', '**/package.json') }}
restore-keys: |
node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-
node-compile-cache-${{ runner.os }}-

- name: Enable Node compile cache
# runner.* is not available in job-level env, so export NODE_COMPILE_CACHE
Expand Down Expand Up @@ -328,11 +366,13 @@ jobs:

- name: Code Coverage
if: ${{ matrix.coverage }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
with:
use_oidc: true

test-egg-scripts:
needs: plan
if: needs.plan.outputs.tests == 'true'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -374,7 +414,6 @@ jobs:
key: node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('pnpm-workspace.yaml', '**/package.json') }}
restore-keys: |
node-compile-cache-${{ runner.os }}-node${{ matrix.node }}-
node-compile-cache-${{ runner.os }}-

- name: Enable Node compile cache
# runner.* is not available in job-level env, so export NODE_COMPILE_CACHE
Expand Down Expand Up @@ -411,11 +450,13 @@ jobs:

- name: Code Coverage
if: ${{ matrix.coverage }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
with:
use_oidc: true

test-tegg-vitest:
needs: plan
if: needs.plan.outputs.tests == 'true'
name: Test tegg-vitest (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -452,13 +493,19 @@ jobs:
run: node node_modules/vitest/vitest.mjs run --root tegg/core/vitest --config vitest.config.ts --pool threads --no-isolate --maxWorkers 1

done:
if: always()
runs-on: ubuntu-latest
needs:
- plan
- coverage
- test
- test-egg-bin
- test-egg-scripts
- test-tegg-vitest
- typecheck
steps:
- run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Require all planned checks
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: node scripts/ci-plan.js check
14 changes: 7 additions & 7 deletions plugins/schedule/test/customTypeError.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { setTimeout as sleep } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll, expect } from 'vitest';
import { describe, it, afterAll, beforeAll, expect, vi } from 'vitest';

import { getFixtures, getLogContent, contains } from './utils.ts';

Expand All @@ -16,9 +14,11 @@ describe.skipIf(process.platform === 'win32')('test/customTypeError.test.ts', ()
afterAll(() => app.close());

it('should work', async () => {
await sleep(process.env.CI ? 10000 : 5000);
const log = getLogContent('customTypeError');
// console.log(log);
expect(contains(log, 'cluster_log')).toBeGreaterThanOrEqual(1);
await vi.waitFor(
() => {
expect(contains(getLogContent('customTypeError'), 'cluster_log')).toBeGreaterThanOrEqual(1);
},
{ timeout: 10000, interval: 100 },
);
});
});
16 changes: 9 additions & 7 deletions plugins/schedule/test/immediate.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setTimeout as sleep } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll, expect } from 'vitest';
import { describe, it, afterAll, beforeAll, expect, vi } from 'vitest';

import { contains, getFixtures, getLogContent } from './utils.ts';

Expand All @@ -16,12 +16,14 @@ describe.skipIf(process.platform === 'win32')('cluster - immediate', () => {
afterAll(() => app.close());

it('should work', async () => {
await sleep(5000);

const log = getLogContent('immediate');
// console.log(log);
expect(contains(log, 'immediate-interval')).toBeGreaterThanOrEqual(2);
expect(contains(log, 'immediate-cron')).toBeGreaterThanOrEqual(2);
await vi.waitFor(
() => {
const log = getLogContent('immediate');
expect(contains(log, 'immediate-interval')).toBeGreaterThanOrEqual(2);
expect(contains(log, 'immediate-cron')).toBeGreaterThanOrEqual(2);
},
{ timeout: 5000, interval: 100 },
);
});
});

Expand Down
12 changes: 7 additions & 5 deletions plugins/schedule/test/scheduleError.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { setTimeout as sleep } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll } from 'vitest';
import { describe, it, afterAll, beforeAll, expect, vi } from 'vitest';

import { getFixtures } from './utils.ts';

Expand All @@ -16,7 +14,11 @@ describe.skipIf(process.platform === 'win32')('test/scheduleError.test.ts', () =
afterAll(() => app.close());

it('should thrown', async () => {
await sleep(5000);
app.expect('stderr', /`schedule\.interval` or `schedule\.cron` or `schedule\.immediate` must be present/);
await vi.waitFor(
() => {
expect(app.stderr).toMatch(/`schedule\.interval` or `schedule\.cron` or `schedule\.immediate` must be present/);
},
{ timeout: 5000, interval: 100 },
);
});
});
28 changes: 16 additions & 12 deletions plugins/schedule/test/subscription.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setTimeout as sleep } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll, expect } from 'vitest';
import { describe, it, afterAll, beforeAll, expect, vi } from 'vitest';

import { contains, getFixtures, getLogContent } from './utils.ts';

Expand All @@ -15,14 +15,15 @@ describe('cluster - subscription', () => {
afterAll(() => app.close());

it('should support interval and cron', async () => {
// interval is 4000ms; give the forked agent/worker boot + IPC + log flush
// enough slack on busy CI runners so the task fires at least once.
await sleep(process.env.CI ? 10000 : 5000);

const log = getLogContent('subscription');
// console.log(log);
expect(contains(log, 'interval')).toBeGreaterThanOrEqual(1);
expect(contains(log, 'cron')).toBeGreaterThanOrEqual(1);
// Keep the real 4-second interval and wait for both schedules to run.
await vi.waitFor(
() => {
const log = getLogContent('subscription');
expect(contains(log, 'interval')).toBeGreaterThanOrEqual(1);
expect(contains(log, 'cron')).toBeGreaterThanOrEqual(1);
},
{ timeout: 10000, interval: 100 },
);
});
});

Expand All @@ -39,9 +40,12 @@ describe('cluster - subscription-generator', () => {
afterAll(() => app.close());

it('should throw error on generator function', async () => {
await sleep(3000);

app.expect('stderr', /"schedule" generator function is not support, should use async function instead/);
await vi.waitFor(
() => {
expect(app.stderr).toMatch(/"schedule" generator function is not support, should use async function instead/);
},
{ timeout: 5000, interval: 100 },
);
});
});

Expand Down
Loading
Loading