[CI] support nightly ci for per pr by labels (#6483)

### What this PR does / why we need it?

This PR refactors the nightly CI workflows (A2 and A3) to support
running tests against a specific PR's code, in addition to the existing
scheduled/dispatch runs using pre-built images.

#### Motivation:
Previously, nightly tests could only be triggered by schedule or
workflow_dispatch, always using the pre-built nightly image. This change
allows developers to trigger nightly tests against their own PR's source
code, enabling early validation without waiting for a nightly build.

#### Changes
Trigger logic (parse-trigger job)

A new parse-trigger job is introduced in both
schedule_nightly_test_a2.yaml and schedule_nightly_test_a3.yaml to
centralize trigger evaluation:

`schedule / workflow_dispatch`: runs all tests with the pre-built image
(existing behavior preserved)
`pull_request (labeled + synchronize)`: runs only when:The PR has the
nightly-test label, and /nightly [test-names] comment exists (latest one
wins)

1. /nightly or /nightly all — runs all tests
2. /nightly test1 test2 — runs only named tests (comma-wrapped for exact
matching)

#### How to trigger
1. Add the nightly-test label to your PR
2. Comment /nightly (all tests) or /nightly test1 test2 (specific tests)
4. Re-triggering: add another /nightly comment and push a new commit
(synchronize event)

### Does this PR introduce _any_ user-facing change?
None

### How was this patch tested?

- vLLM version: v0.14.1
- vLLM main:
dc917cceb8

---------

Signed-off-by: hfadzxy <starmoon_zhang@163.com>
This commit is contained in:
zhangxinyuehfad
2026-03-05 16:46:37 +08:00
committed by GitHub
parent a6745b8577
commit 1e4017e3fa
8 changed files with 490 additions and 32 deletions

View File

@@ -25,10 +25,15 @@ on:
# Run test at 24:00 Beijing time (UTC+8)
- cron: "0 16 * * *"
workflow_dispatch:
pull_request:
pull_request:
branches:
- 'main'
types: [ labeled ]
types: [ labeled, synchronize ]
permissions:
contents: read
pull-requests: read
issues: read
# Bash shells do not use ~/.profile or ~/.bashrc so these shells need to be explicitly
# declared as "shell: bash -el {0}" on steps that need to be properly activated.
@@ -42,9 +47,84 @@ concurrency:
cancel-in-progress: true
jobs:
parse-trigger:
name: Parse trigger and determine test scope
runs-on: linux-aarch64-a2b3-0
outputs:
should_run: ${{ steps.parse.outputs.should_run }}
test_filter: ${{ steps.parse.outputs.test_filter }}
is_pr_event: ${{ steps.parse.outputs.is_pr_event }}
steps:
- name: Parse trigger
id: parse
uses: actions/github-script@v7
with:
script: |
const eventName = context.eventName;
function parseNightlyComment(body) {
if (!body) return null;
const match = body.trim().match(/^\/nightly(?:\s+(.+))?$/m);
if (!match) return null;
const args = (match[1] || '').trim();
if (!args || args === 'all') return 'all';
// Wrap with commas for exact-name matching: ",name1,name2,"
return ',' + args.split(/\s+/).join(',') + ',';
}
// schedule / workflow_dispatch: run all tests with pre-built image
if (eventName === 'schedule' || eventName === 'workflow_dispatch') {
core.setOutput('should_run', 'true');
core.setOutput('test_filter', 'all');
core.setOutput('is_pr_event', 'false');
return;
}
// pull_request (labeled / synchronize)
if (eventName === 'pull_request') {
const labels = context.payload.pull_request.labels.map(l => l.name);
if (!labels.includes('nightly-test')) {
core.setOutput('should_run', 'false');
core.setOutput('test_filter', '');
core.setOutput('is_pr_event', 'true');
return;
}
// Search comments for latest /nightly command
const prNumber = context.payload.pull_request.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
let testFilter = null;
for (let i = comments.length - 1; i >= 0; i--) {
const result = parseNightlyComment(comments[i].body);
if (result !== null) { testFilter = result; break; }
}
// No /nightly comment found: do not run any tests
if (testFilter === null) {
core.info('nightly-test label present but no /nightly comment found; skipping.');
core.setOutput('should_run', 'false');
core.setOutput('test_filter', '');
core.setOutput('is_pr_event', 'true');
return;
}
core.setOutput('should_run', 'true');
core.setOutput('test_filter', testFilter);
core.setOutput('is_pr_event', 'true');
return;
}
// Fallback
core.setOutput('should_run', 'false');
core.setOutput('test_filter', '');
core.setOutput('is_pr_event', 'false');
multi-node-tests:
name: multi-node
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [parse-trigger]
if: always() && needs.parse-trigger.outputs.should_run == 'true'
strategy:
fail-fast: false
max-parallel: 1
@@ -106,13 +186,28 @@ jobs:
replicas: 1
size: ${{ matrix.test_config.size }}
config_file_path: ${{ matrix.test_config.config_file_path }}
vllm_ascend_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref_name }}
is_pr_test: >-
${{
needs.parse-trigger.outputs.is_pr_event == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
is_run: >-
${{
needs.parse-trigger.outputs.should_run == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
secrets:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
single-node-tests:
name: single-node
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [multi-node-tests]
needs: [parse-trigger, multi-node-tests]
if: always() && needs.parse-trigger.outputs.should_run == 'true'
strategy:
fail-fast: false
matrix:
@@ -126,11 +221,25 @@ jobs:
image: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:nightly-a3'
tests: ${{ matrix.test_config.tests }}
name: ${{ matrix.test_config.name }}
is_pr_test: >-
${{
needs.parse-trigger.outputs.is_pr_event == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
is_run: >-
${{
needs.parse-trigger.outputs.should_run == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
single-node-yaml-tests:
name: single-node
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [multi-node-tests]
if: always() && needs.parse-trigger.outputs.should_run == 'true'
needs: [parse-trigger, multi-node-tests]
strategy:
fail-fast: false
matrix:
@@ -190,11 +299,25 @@ jobs:
image: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:nightly-a3'
config_file_path: ${{ matrix.test_config.config_file_path }}
name: ${{ matrix.test_config.name }}
is_pr_test: >-
${{
needs.parse-trigger.outputs.is_pr_event == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
is_run: >-
${{
needs.parse-trigger.outputs.should_run == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
custom-ops-tests:
name: test ops
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: multi-node-tests
needs: [parse-trigger, multi-node-tests]
if: always() && needs.parse-trigger.outputs.should_run == 'true'
strategy:
fail-fast: false
matrix:
@@ -208,3 +331,17 @@ jobs:
image: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:nightly-a3'
tests: ${{ matrix.test_config.tests }}
name: ${{ matrix.test_config.name }}
is_pr_test: >-
${{
needs.parse-trigger.outputs.is_pr_event == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}
is_run: >-
${{
needs.parse-trigger.outputs.should_run == 'true' && (
needs.parse-trigger.outputs.test_filter == 'all' ||
contains(needs.parse-trigger.outputs.test_filter, format(',{0},', matrix.test_config.name))
)
}}