mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-11 17:55:44 +00:00
* fix(ci): stop cancelling in-progress E2E runs on main Merges land on main roughly every 18 minutes (median) while a full E2E run takes about 40, so cancelling the in-flight run on every push starved the suite: across the last 100 push runs, 67 were cancelled and only 25 ever reported a result. The nightly regression was the only reliable signal, and each cancelled run still burned roughly 10 minutes on three runners before dying. Turning cancellation off for main hands the coalescing to GitHub's concurrency queue, which keeps at most one pending run per group and cancels the previously pending one. The queue therefore collapses to the newest tree on its own while the in-flight run always finishes, so every result covers the batch of commits merged since the last one — bisect that range when it goes red. Dev branches keep cancelling superseded runs, where only the latest push matters. * fix(ci): dedupe main CI failure issues by failing test The autofix issue for a red main was deduped on the commit SHA, so a standing failure opened a brand new issue on every merge: one broken E2E test on 2026-07-26 produced six duplicate issues in twelve hours, each pointing the autofix agent at an unrelated commit. The failing tests are now identified from the logs of the failed jobs and used as the dedupe key, with one marker per test so a failure set that grows still matches the issue that already tracks part of it. Later commits hitting the same failure are appended to that issue as recurrences (bounded, newest first) instead of opening another one, and notes written by a human or the agent are preserved when the machine-owned trailer is refreshed. Runs with no identifiable test — an install or build break — keep the previous per-commit behaviour. * fix(ci): keep the failure analysis out of the bot-PAT job Identifying the failing tests means running a helper from the repository, and the workflow deliberately checked out nothing so that the job holding the bot PAT could never execute repository code — an invariant its own test enforces. Rather than weaken it, the work is split: a read-only job checks out the tree, reads the failed run's logs, finds any issue that already tracks the failure and renders the title and body, handing both over as outputs. The job with the PAT keeps checking out nothing and only writes what it was given. The invariant test now asserts that separation — the PAT job runs no repository code and holds only issue write — instead of banning checkout everywhere in the file. * fix(ci): harden main CI failure dedupe per review (#7795) - Match the `[run <id>]` link text instead of the run URL when deduping recurrences: `/301` is a substring of `/3010`, so the URL match silently deleted an unrelated run's line. - Rebuild the machine-owned "## Also failing" section from the live failure set on every merge so a test that has since been fixed drops out instead of being listed forever. - Assert the analyze checkout is SHA-pinned with persist-credentials disabled and pin the failed-job log-download paths in the workflow test. - Add an e2e workflow test guarding the cancel-in-progress expression that keeps in-progress runs on main from being cancelled. * fix(ci): bound the issue body and randomize the heredoc delimiter (#7795) * fix(ci): test the runCli --existing merge path (#7795) * fix(ci): address review feedback on e2e signal PR (#7795) - Assert the full cancel-in-progress expression including && so a mutation to || is caught by the e2e-workflow test - Filter the capped-summary line from missingTests so it is not rendered as a fake bullet under Also failing --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
122 lines
5.1 KiB
JavaScript
122 lines
5.1 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { parse } from 'yaml';
|
|
|
|
describe('main CI failure issue workflow', () => {
|
|
const workflow = readFileSync(
|
|
'.github/workflows/main-ci-failure-issue.yml',
|
|
'utf8',
|
|
);
|
|
const yml = parse(workflow);
|
|
const jobs = yml.jobs;
|
|
|
|
it('opens an autofix-ready issue only for failed main CI runs', () => {
|
|
expect(workflow).toContain('workflow_run:');
|
|
expect(workflow).toContain("workflows: ['E2E Tests', 'SDK Python']");
|
|
expect(workflow).not.toContain("'Qwen Code CI'");
|
|
expect(workflow).toContain("types: ['completed']");
|
|
expect(workflow).toContain("github.repository == 'QwenLM/qwen-code'");
|
|
expect(workflow).toContain(
|
|
"github.event.workflow_run.conclusion == 'failure'",
|
|
);
|
|
expect(workflow).toContain(
|
|
"github.event.workflow_run.head_branch == 'main'",
|
|
);
|
|
expect(workflow).toContain("github.event.workflow_run.event == 'push'");
|
|
});
|
|
|
|
it('creates an issue that the existing autofix worker can pick up', () => {
|
|
expect(workflow).toContain("issues: 'write'");
|
|
expect(workflow).toContain('CI_DEV_BOT_PAT');
|
|
expect(workflow).toContain(
|
|
'AUTOFIX_BOT: "${{ vars.AUTOFIX_BOT_LOGIN || \'qwen-code-dev-bot\' }}"',
|
|
);
|
|
expect(workflow).toContain("BUG_LABEL: 'type/bug'");
|
|
expect(workflow).toContain(
|
|
"READY_FOR_AGENT_LABEL: 'status/ready-for-agent'",
|
|
);
|
|
expect(workflow).toContain("AUTOFIX_APPROVED_LABEL: 'autofix/approved'");
|
|
expect(workflow).toContain('gh issue edit "$1"');
|
|
expect(workflow).toContain(
|
|
'--add-label "${BUG_LABEL},${READY_FOR_AGENT_LABEL},${AUTOFIX_APPROVED_LABEL}"',
|
|
);
|
|
expect(workflow).toContain('--add-assignee "${AUTOFIX_BOT}"');
|
|
expect(workflow).toContain('apply_autofix_route "${issue_url}"');
|
|
});
|
|
|
|
it('deduplicates by failing test and includes run context', () => {
|
|
// The dedupe key is the failing test, not the commit: a standing red used to
|
|
// open one issue per merge. The markers themselves live in the helper.
|
|
expect(workflow).toContain('main-failure-signature.mjs');
|
|
expect(workflow).toContain('searchMarkers');
|
|
// The failing tests are read from the triggering run's failed-job logs, so
|
|
// the dedupe key is recovered even when the run reported no test result.
|
|
expect(workflow).toContain('actions/runs/${WORKFLOW_RUN_ID}/jobs');
|
|
expect(workflow).toContain('actions/jobs/${job_id}/logs');
|
|
expect(workflow).toContain('gh issue list');
|
|
expect(workflow).toContain('gh issue create');
|
|
expect(workflow).toContain('apply_autofix_route "${EXISTING_ISSUE}"');
|
|
expect(workflow).toContain('${WORKFLOW_RUN_URL}');
|
|
expect(workflow).toContain('${HEAD_SHA}');
|
|
});
|
|
|
|
it('re-reads an existing issue so recorded recurrences survive the update', () => {
|
|
expect(workflow).toContain('gh issue view "${existing_issue}"');
|
|
expect(workflow).toContain('--existing "${existing_body}"');
|
|
});
|
|
|
|
it('uses a random heredoc delimiter for the multiline body output', () => {
|
|
// A constant delimiter lets issue-body prose (which the autofix agent
|
|
// writes into) end the heredoc early and inject fresh GITHUB_OUTPUT keys.
|
|
expect(workflow).toContain('openssl rand -hex 16');
|
|
expect(workflow).toContain('echo "body<<${delim}"');
|
|
expect(workflow).toContain('echo "${delim}"');
|
|
expect(workflow).not.toContain('body<<QWEN_MAIN_CI_FAILURE_BODY\n');
|
|
});
|
|
|
|
const privilegedJobs = Object.entries(jobs).filter(([, job]) =>
|
|
JSON.stringify(job).includes('CI_DEV_BOT_PAT'),
|
|
);
|
|
|
|
it('keeps the bot PAT in a job that runs no repository code', () => {
|
|
// The job that can write as the bot must not check out or execute anything
|
|
// from the repository; it only consumes strings produced elsewhere.
|
|
expect(privilegedJobs).toHaveLength(1);
|
|
for (const [name, job] of privilegedJobs) {
|
|
const rendered = JSON.stringify(job);
|
|
expect(rendered, name).not.toContain('actions/checkout');
|
|
expect(rendered, name).not.toContain('main-failure-signature.mjs');
|
|
expect(job.permissions, name).toEqual({ issues: 'write' });
|
|
}
|
|
});
|
|
|
|
it('pins the analyze checkout and drops persist-credentials', () => {
|
|
// The read-only analyze job does check out the repo (it runs the helper),
|
|
// so pin it to a SHA rather than a mutable tag and never leave the workflow
|
|
// token on the runner.
|
|
const checkout = jobs.analyze.steps.find((step) =>
|
|
String(step.uses ?? '').startsWith('actions/checkout'),
|
|
);
|
|
expect(checkout).toBeDefined();
|
|
expect(checkout.uses).toMatch(/^actions\/checkout@[0-9a-f]{40}$/);
|
|
expect(checkout.with['persist-credentials']).toBe(false);
|
|
});
|
|
|
|
it('keeps the log analysis away from the bot PAT and from write scopes', () => {
|
|
const analyze = jobs.analyze;
|
|
expect(JSON.stringify(analyze)).not.toContain('CI_DEV_BOT_PAT');
|
|
// Reading job logs needs `actions: read`; nothing here needs write.
|
|
expect(analyze.permissions).toEqual({
|
|
actions: 'read',
|
|
contents: 'read',
|
|
issues: 'read',
|
|
});
|
|
expect(privilegedJobs[0][1].needs).toBe('analyze');
|
|
});
|
|
});
|