mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-06 19:06:58 +00:00
* ci: reduce duplicate and PR-scoped Rust cache writes Signed-off-by: WenyXu <wenymedia@gmail.com> * ci: warm shared caches through scheduled integration jobs Signed-off-by: WenyXu <wenymedia@gmail.com> --------- Signed-off-by: WenyXu <wenymedia@gmail.com>
156 lines
7.2 KiB
JavaScript
156 lines
7.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { readFileSync } = require('node:fs');
|
|
const { resolve } = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const directory = resolve(__dirname, '../../workflows');
|
|
const workflows = ['checks', 'rust', 'integration', 'nightly-ci'].map(name => ({
|
|
name,
|
|
text: readFileSync(resolve(directory, name + '.yml'), 'utf8'),
|
|
}));
|
|
function jobs(text) {
|
|
return [...text.matchAll(/^ ([\w-]+):\n([\s\S]*?)(?=^ [\w-]+:\n|(?![\s\S]))/gm)];
|
|
}
|
|
function steps(body) {
|
|
return body.split(/^ - /m).slice(1);
|
|
}
|
|
|
|
test('build caches have one controller and lightweight checks do not cache targets', () => {
|
|
let checked = 0;
|
|
for (const { name, text } of workflows) {
|
|
for (const [, job, body] of jobs(text)) {
|
|
if (!body.includes('Swatinem/rust-cache@') && !['fmt', 'check-udeps', 'toml'].includes(job)) continue;
|
|
for (const step of steps(body).filter(s => s.includes('uses: actions-rust-lang/setup-rust-toolchain@'))) {
|
|
assert.match(step, /^ cache: false$/m, name + '/' + job);
|
|
checked++;
|
|
}
|
|
}
|
|
}
|
|
assert.ok(checked >= 10, 'expected build and lightweight jobs to be covered');
|
|
});
|
|
|
|
test('only main writes caches, and the nightly read-only cache stays read-only', () => {
|
|
let checked = 0;
|
|
for (const { name, text } of workflows) {
|
|
for (const [, job, body] of jobs(text)) {
|
|
for (const step of steps(body)) {
|
|
if (!step.includes('uses: Swatinem/rust-cache@') && !step.includes('uses: actions/cache/save@')) continue;
|
|
const condition = step.includes('uses: Swatinem/rust-cache@')
|
|
? step.match(/^ save-if: (.+)$/m)?.[1]
|
|
: step.match(/^ if: (.+)$/m)?.[1];
|
|
assert.ok(condition, name + '/' + job);
|
|
const expression = condition.replace(/^\$\{\{\s*|\s*\}\}$/g, '')
|
|
.replaceAll('steps.fuzz-binaries.outputs.cache-hit', 'hit');
|
|
const enabled = new Function('github', 'hit', 'return (' + expression + ');');
|
|
for (const [event, ref] of [
|
|
['pull_request', 'refs/pull/123/merge'],
|
|
['merge_group', 'refs/heads/gh-readonly-queue/main/pr-123'],
|
|
['workflow_dispatch', 'refs/heads/feature'],
|
|
]) assert.equal(enabled({ event_name: event, ref }, 'false'), false, name + '/' + job);
|
|
assert.equal(enabled({ event_name: 'schedule', ref: 'refs/heads/main' }, 'false'),
|
|
condition !== 'false', name + '/' + job);
|
|
if (step.includes('uses: actions/cache/save@')) {
|
|
assert.equal(enabled({ ref: 'refs/heads/main' }, 'true'), false);
|
|
}
|
|
checked++;
|
|
}
|
|
}
|
|
}
|
|
assert.ok(checked >= 10);
|
|
});
|
|
|
|
test('helper tool versions do not fragment unrelated Rust caches', () => {
|
|
for (const { text } of workflows) {
|
|
assert.doesNotMatch(text, /CARGO_(SHEAR|FUZZ)_VERSION/);
|
|
}
|
|
const integration = workflows.find(w => w.name === 'integration').text;
|
|
assert.match(integration, /prefix-key: "v1-fuzz-\$\{\{ env.FUZZ_VERSION \}\}"/);
|
|
assert.match(integration, /cargo install cargo-fuzz --version "\$\{FUZZ_VERSION\}"/);
|
|
});
|
|
|
|
// Evaluate the actual job guard as well as the cache's save condition. A valid
|
|
// save-if alone cannot prove that a scheduled cache producer is reachable.
|
|
function warmupJob(workflow, job, {
|
|
event = 'schedule', ref = 'refs/heads/main',
|
|
profile = event === 'workflow_dispatch' ? 'all' : undefined,
|
|
outputs = {}, failed = '', cancelled = false, draft = false,
|
|
} = {}) {
|
|
const text = workflows.find(w => w.name === workflow).text;
|
|
assert.match(text, /^ schedule:$/m);
|
|
const body = jobs(text).find(([, name]) => name === job)?.[2];
|
|
assert.ok(body, workflow + '/' + job);
|
|
const github = { repository: 'GreptimeTeam/greptimedb', event_name: event, ref,
|
|
event: { pull_request: { draft } }, run_attempt: 1 };
|
|
const needs = { scope: { outputs }, ...Object.fromEntries(
|
|
['github-script-tests', 'build', 'prepare-fuzz-targets', 'build-greptime-ci'].map(name =>
|
|
[name, { result: name === failed ? 'failure' : 'success' }])) };
|
|
const evaluate = expression => new Function('github', 'inputs', 'needs', 'cancelled',
|
|
'return (' + expression.replace(/^\$\{\{\s*|\s*\}\}$/g, '') + ');')(
|
|
github, { fuzz_profile: profile, ci_command: false }, needs, () => cancelled);
|
|
const admitted = evaluate(body.match(/^ if: (.+)$/m)[1]);
|
|
const saves = [...body.matchAll(/^ save-if: (.+)$/gm)]
|
|
.map(match => admitted && evaluate(match[1]));
|
|
return { admitted, saves };
|
|
}
|
|
|
|
const fuzzJobs = ['prepare-fuzz-targets', 'build-greptime-ci', 'fuzztest',
|
|
'unstable-fuzztest', 'distributed-fuzztest', 'distributed-fuzztest-with-chaos'];
|
|
|
|
test('main schedules can execute cache producers and their fuzz consumers', () => {
|
|
for (const job of fuzzJobs) {
|
|
// Scheduled events do not supply workflow_dispatch inputs.
|
|
const result = warmupJob('integration', job, { profile: undefined });
|
|
assert.equal(result.admitted, true, job);
|
|
assert.ok(result.saves.every(Boolean), job);
|
|
}
|
|
for (const [workflow, job] of [
|
|
['integration', 'build'], ['integration', 'export-import-v2-e2e'],
|
|
['integration', 'prepare-fuzz-targets'], ['integration', 'build-greptime-ci'],
|
|
['integration', 'distributed-fuzztest'],
|
|
['rust', 'check-windows'], ['rust', 'check-riscv64'],
|
|
['nightly-ci', 'sqlness-windows'], ['nightly-ci', 'test-on-windows'],
|
|
]) {
|
|
for (const event of ['schedule', 'workflow_dispatch']) {
|
|
assert.deepEqual(warmupJob(workflow, job, { event }),
|
|
{ admitted: true, saves: [true] }, workflow + '/' + job + '/' + event);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('manual warming respects selected profiles without an internal command marker', () => {
|
|
const expected = {
|
|
none: [],
|
|
standalone: fuzzJobs.slice(0, 4),
|
|
distributed: [fuzzJobs[0], fuzzJobs[1], fuzzJobs[4]],
|
|
chaos: [fuzzJobs[0], fuzzJobs[1], fuzzJobs[5]],
|
|
all: fuzzJobs,
|
|
};
|
|
for (const [profile, wanted] of Object.entries(expected)) {
|
|
assert.deepEqual(fuzzJobs.filter(job => warmupJob('integration', job,
|
|
{ event: 'workflow_dispatch', profile }).admitted), wanted, profile);
|
|
}
|
|
for (const job of fuzzJobs) {
|
|
const result = warmupJob('integration', job,
|
|
{ event: 'workflow_dispatch', ref: 'refs/heads/feature' });
|
|
assert.ok(result.saves.every(value => value === false), job);
|
|
assert.equal(warmupJob('integration', job,
|
|
{ event: 'pull_request', ref: 'refs/pull/1/merge', draft: true }).admitted, false, job);
|
|
}
|
|
});
|
|
|
|
test('warmup keeps scope exclusions, dependency failures and cancellation gates', () => {
|
|
const gates = ['any_fuzz', 'build_ci_needed', 'fuzz_standalone',
|
|
'fuzz_unstable', 'fuzz_distributed', 'fuzz_chaos'];
|
|
for (const [index, job] of fuzzJobs.entries()) {
|
|
assert.equal(warmupJob('integration', job, { failed: 'github-script-tests' }).admitted, false, job);
|
|
assert.equal(warmupJob('integration', job, { cancelled: true }).admitted, false, job);
|
|
assert.equal(warmupJob('integration', job,
|
|
{ outputs: { [gates[index]]: 'false' } }).admitted, false, job);
|
|
}
|
|
for (const job of fuzzJobs.slice(2)) {
|
|
for (const failed of ['github-script-tests', 'prepare-fuzz-targets', job === 'fuzztest' ? 'build' : 'build-greptime-ci']) {
|
|
assert.equal(warmupJob('integration', job, { failed }).admitted, false, job);
|
|
}
|
|
}
|
|
});
|