mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-06 19:06:58 +00:00
ci: reduce repeated Windows and fuzz tests on pull requests (#10043)
* ci: reduce repeated Windows and fuzz tests on pull requests Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: use the nightly merge label as the enqueue opt-in Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: log nightly merge decisions for dry-run verification Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: gate expensive jobs on GitHub script tests Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: grant pull request write permission for nightly label sync Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: run platform checks on schedule or manual dispatch Signed-off-by: WenyXu <wenymedia@gmail.com> * test(ci): update PR gate count for scheduled platform checks Signed-off-by: WenyXu <wenymedia@gmail.com> --------- Signed-off-by: WenyXu <wenymedia@gmail.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
const { readFileSync } = require('node:fs');
|
||||
const { resolve } = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const workflows = resolve(__dirname, '../workflows');
|
||||
const integration = readFileSync(resolve(workflows, 'integration.yml'), 'utf8');
|
||||
const suites = ['fuzztest', 'unstable-fuzztest', 'distributed-fuzztest', 'distributed-fuzztest-with-chaos'];
|
||||
const prerequisites = ['prepare-fuzz-targets', 'build-greptime-ci'];
|
||||
function enabled(job, { event = 'pull_request', action = 'synchronize', draft = false, attempt = 1, profile = 'none', repository = 'GreptimeTeam/greptimedb', outputs = {}, results = {}, cancelled = false, workflow = integration } = {}) {
|
||||
const block = workflow.split(` ${job}:\n`)[1].split(/\n [a-z][\w-]*:/)[0];
|
||||
if (block.includes("needs['github-script-tests']")) {
|
||||
assert.match(block, /^ needs: \[.*github-script-tests.*\]/m);
|
||||
}
|
||||
const expression = block.match(/^ if: \$\{\{ (.+) \}\}/m)[1];
|
||||
// These job guards use only operators whose semantics match JS for this fixture.
|
||||
return new Function('github', 'inputs', 'needs', 'cancelled', `return (${expression});`)(
|
||||
{ repository, event_name: event, event: { action, pull_request: { draft } }, run_attempt: attempt },
|
||||
{ fuzz_profile: profile, ci_command: false },
|
||||
{ scope: { outputs }, ...Object.fromEntries(['github-script-tests', 'build', ...prerequisites].map(name => [name, { result: results[name] || 'success' }])) },
|
||||
() => cancelled
|
||||
);
|
||||
}
|
||||
test('fuzz event policy on ready PR admission and schedule, not every update', () => {
|
||||
for (const job of [...suites, ...prerequisites]) {
|
||||
for (const action of ['opened', 'ready_for_review']) assert.equal(enabled(job, { action }), true, job);
|
||||
for (const action of ['synchronize', 'reopened']) assert.equal(enabled(job, { action }), false, job);
|
||||
assert.equal(enabled(job, { event: 'schedule' }), true, job);
|
||||
assert.equal(enabled(job, { event: 'merge_group' }), false, job);
|
||||
assert.equal(enabled(job, { attempt: 2 }), false, job);
|
||||
assert.equal(enabled(job, { action: 'opened', draft: true }), false, job);
|
||||
assert.equal(enabled(job, { attempt: 2, draft: true }), false, job);
|
||||
assert.equal(enabled(job, { event: 'schedule', repository: 'fork/greptimedb' }), false, job);
|
||||
}
|
||||
});
|
||||
test('manual profiles select only their suite and all required build jobs', () => {
|
||||
const expected = { none: [], standalone: suites.slice(0, 2), distributed: [suites[2]], chaos: [suites[3]], all: suites };
|
||||
for (const [profile, jobs] of Object.entries(expected)) {
|
||||
assert.deepEqual(suites.filter(job => enabled(job, { event: 'workflow_dispatch', profile })), jobs);
|
||||
for (const job of prerequisites) assert.equal(enabled(job, { event: 'workflow_dispatch', profile }), profile !== 'none');
|
||||
}
|
||||
});
|
||||
test('core integration tests still run on PR updates and merge groups', () => {
|
||||
for (const job of ['build', 'sqlness', 'export-import-v2-e2e', 'run-multi-lang-tests']) {
|
||||
assert.equal(enabled(job), true, job);
|
||||
assert.equal(enabled(job, { event: 'merge_group' }), true, job);
|
||||
}
|
||||
});
|
||||
test('Windows compilation stays in Rust CI; test coverage remains in Nightly CI', () => {
|
||||
const rust = readFileSync(resolve(workflows, 'rust.yml'), 'utf8');
|
||||
const nightly = readFileSync(resolve(workflows, 'nightly-ci.yml'), 'utf8');
|
||||
assert.match(rust, /^ check-windows:/m);
|
||||
assert.doesNotMatch(rust, /^ test-windows:/m);
|
||||
assert.match(nightly, /^ test-on-windows:/m);
|
||||
assert.match(nightly, /^ sqlness-windows:/m);
|
||||
assert.match(nightly, /cargo nextest run -F dashboard/);
|
||||
assert.match(nightly, /^ workflow_dispatch:/m);
|
||||
});
|
||||
|
||||
test('scope exclusions and dependency failures compose with the event policy', () => {
|
||||
const gates = {
|
||||
'prepare-fuzz-targets': 'any_fuzz', 'build-greptime-ci': 'build_ci_needed',
|
||||
fuzztest: 'fuzz_standalone', 'unstable-fuzztest': 'fuzz_unstable',
|
||||
'distributed-fuzztest': 'fuzz_distributed', 'distributed-fuzztest-with-chaos': 'fuzz_chaos'
|
||||
};
|
||||
for (const [job, gate] of Object.entries(gates)) {
|
||||
assert.equal(enabled(job, { action: 'opened', outputs: { [gate]: 'false' } }), false, job);
|
||||
assert.equal(enabled(job, { action: 'opened', outputs: {} }), true, job);
|
||||
assert.equal(enabled(job, { outputs: { [gate]: 'true' } }), false, job);
|
||||
assert.equal(enabled(job, { action: 'opened', cancelled: true }), false, job);
|
||||
}
|
||||
for (const job of suites) {
|
||||
for (const result of ['failure', 'skipped', 'cancelled']) {
|
||||
assert.equal(enabled(job, { action: 'opened', results: { 'prepare-fuzz-targets': result } }), false, job);
|
||||
const build = job === 'fuzztest' ? 'build' : 'build-greptime-ci';
|
||||
assert.equal(enabled(job, { action: 'opened', results: { [build]: result } }), false, job);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('script tests must pass before any expensive integration job can start', () => {
|
||||
const jobs = ['build', 'sqlness', 'sqlness-kvbackend', 'export-import-v2-e2e',
|
||||
'run-multi-lang-tests', 'compat', ...prerequisites, ...suites];
|
||||
for (const job of jobs) {
|
||||
assert.equal(enabled(job, { action: 'opened' }), true, job);
|
||||
for (const result of ['failure', 'skipped', 'cancelled']) {
|
||||
assert.equal(enabled(job, { action: 'opened', results: { 'github-script-tests': result } }), false, job);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('script tests gate expensive Rust and Checks jobs', () => {
|
||||
for (const [file, jobs] of [
|
||||
['rust.yml', ['clippy', 'check-riscv64', 'check-windows', 'test', 'coverage']],
|
||||
['checks.yml', ['check']]
|
||||
]) {
|
||||
const workflow = readFileSync(resolve(workflows, file), 'utf8');
|
||||
for (const job of jobs) {
|
||||
const event = job === 'coverage' ? 'merge_group' : 'workflow_dispatch';
|
||||
assert.equal(enabled(job, { workflow, event }), true, job);
|
||||
for (const result of ['failure', 'skipped', 'cancelled']) {
|
||||
assert.equal(enabled(job, { workflow, event, results: { 'github-script-tests': result } }), false, job);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -61,16 +61,22 @@ async function run({ sync = false, body = checked, labels = ['merge nightly'],
|
||||
const summary = { addHeading() { return this; }, addTable(rows) { calls.push(['summary', rows]); return this; }, async write() {} };
|
||||
await scripts[sync ? 0 : 1](github,
|
||||
{ repo: { owner: 'GreptimeTeam', repo: 'greptimedb' }, payload: { pull_request: { number: 1 } } },
|
||||
{ summary, warning() {}, setFailed: message => calls.push(['failed', message]) },
|
||||
{ summary, info() {}, warning() {}, setFailed: message => calls.push(['failed', message]) },
|
||||
{ env: { DRY_RUN: String(dryRun) } });
|
||||
return calls;
|
||||
}
|
||||
|
||||
test('label sync grants PR label write permission without checking out PR code', () => {
|
||||
const syncJob = workflow.split(' sync-label:\n')[1].split(' enqueue:\n')[0];
|
||||
assert.match(syncJob, /^ pull-requests: write$/m);
|
||||
assert.doesNotMatch(syncJob, /uses: actions\/checkout/);
|
||||
});
|
||||
|
||||
test('template opts in and checkbox edits reconcile only the nightly label', async () => {
|
||||
assert.ok(readFileSync(resolve(__dirname, '../../pull_request_template.md'), 'utf8').includes(checked));
|
||||
assert.deepEqual(await run({ sync: true, labels: ['bug'] }), [['add', ['merge nightly']]]);
|
||||
assert.deepEqual(await run({ sync: true }), []);
|
||||
for (const body of [checked.replace('[x]', '[ ]'), '', null, `> ${checked}`]) {
|
||||
for (const body of [checked.replace('[x]', '[ ]')]) {
|
||||
assert.deepEqual(await run({ sync: true, body }), [['remove', 'merge nightly']]);
|
||||
}
|
||||
assert.deepEqual(await run({ sync: true, body: checked.replace('[x]', '[X]'), labels: [] }), [['add', ['merge nightly']]]);
|
||||
@@ -83,7 +89,7 @@ test('eligible PR is enqueued with its checked head; dry run performs no mutatio
|
||||
|
||||
test('each eligibility gate independently prevents enqueueing', async () => {
|
||||
for (const options of [
|
||||
{ body: checked.replace('[x]', '[ ]') }, { labels: [] },
|
||||
{ labels: [] },
|
||||
{ current: { draft: true } }, { current: { state: 'closed' } }, { current: { base: { ref: 'release/v1.3' } } },
|
||||
{ detail: { reviewDecision: 'REVIEW_REQUIRED' } }, { detail: { reviewDecision: 'CHANGES_REQUESTED' } },
|
||||
{ detail: { mergeStateStatus: 'BLOCKED' } }, { detail: { mergeStateStatus: 'UNKNOWN' } },
|
||||
@@ -106,3 +112,14 @@ test('per-run cap and individual failures do not prevent later candidates', asyn
|
||||
assert.equal(calls.filter(c => c[0] === 'enqueue').length, 2);
|
||||
assert.equal(calls.filter(c => c[0] === 'failed').length, 1);
|
||||
});
|
||||
|
||||
test('label-only opt-in survives PRs without a template and does not require a checkbox', async () => {
|
||||
for (const body of ['', null, 'Existing PR description', `> ${checked}`]) {
|
||||
assert.deepEqual(await run({ sync: true, body }), []);
|
||||
assert.deepEqual(await run({ sync: true, body, labels: [] }), []);
|
||||
assert.equal((await run({ body })).filter(call => call[0] === 'enqueue').length, 1);
|
||||
assert.equal((await run({ body, labels: [] })).some(call => call[0] === 'enqueue'), false);
|
||||
}
|
||||
// The label is authoritative for admission, even before checkbox synchronization.
|
||||
assert.equal((await run({ body: checked.replace('[x]', '[ ]') })).some(call => call[0] === 'enqueue'), true);
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ class CiSlashTest(unittest.TestCase):
|
||||
|
||||
def test_fork_rerun_gates_cover_standard_jobs(self):
|
||||
import re
|
||||
for name, count in [("rust", 7), ("integration", 7), ("checks", 6), ("docs", 3)]:
|
||||
for name, count in [("rust", 6), ("integration", 8), ("checks", 6), ("docs", 3)]:
|
||||
workflow = (SCRIPT.parents[1] / "workflows" / (name + ".yml")).read_text()
|
||||
gates = re.findall(r"^ if:.*github.run_attempt > 1.*$", workflow, re.MULTILINE)
|
||||
self.assertEqual(len(gates), count, name)
|
||||
|
||||
Reference in New Issue
Block a user