mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-02 21:04:32 +00:00
* fix(release): raise model timeouts and shrink batch size for slow networks The AI release-notes generator timed out on every batch in the v0.21.1 finalize run on a GitHub-hosted runner (US, Azure westus3) hitting a remote LLM endpoint. The 60s per-request timeout was too close to the edge: two successful requests took 54.2s and 56.7s, and every other batch hit the 60s abort. The 12-minute total budget was consumed by retries on those timeouts, and the circuit breaker opened after 3 consecutive failures, skipping highlights entirely. - timeoutMs: 60s -> 180s — give the model enough headroom to generate a full JSON response over a high-RTT cross-region connection. - totalTimeoutMs: 12min -> 30min — ~140 PRs at 8 per batch is ~18 batches; at ~90s each that's ~27 minutes of model time. - batchSize: 12 -> 8 — fewer entries per prompt means the model generates less per request, returning faster and reducing the chance of a single slow request dragging the whole batch. - workflow timeout-minutes: 15 -> 35 — match the new 30min budget plus a margin for git/npm setup. No changes to retry logic, circuit breaker, or prompt shape. * fix(release): sync timeout workflow test * test(release): assert step timeout exceeds script budget The workflow `timeout-minutes` and the script's `totalTimeoutMs` are defined in separate files with no shared constant. If they drift apart silently and the budget exceeds the step timeout, the runner SIGKILLs the step and even the fallback release notes are lost. Add a cross-file assertion so any future change to either value that breaks the invariant fails the test immediately. Suggested-by: wenshao in PR review. * test(release): parse release notes timeout expression
144 lines
5.4 KiB
JavaScript
144 lines
5.4 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const releaseWorkflow = readFileSync('.github/workflows/release.yml', 'utf8');
|
|
const finalizeWorkflow = readFileSync(
|
|
'.github/workflows/finalize-release.yml',
|
|
'utf8',
|
|
);
|
|
const releaseNotesScript = readFileSync(
|
|
'scripts/generate-release-notes.js',
|
|
'utf8',
|
|
);
|
|
|
|
function getStep(workflow, name) {
|
|
const match = new RegExp(
|
|
`\\n - name: '${name}'[\\s\\S]*?(?=\\n - name: '|\\n [A-Za-z0-9_-]+:|$)`,
|
|
).exec(`\n${workflow}`);
|
|
if (!match) {
|
|
throw new Error(`Could not find workflow step: ${name}`);
|
|
}
|
|
return match[0];
|
|
}
|
|
|
|
describe('stable release notes workflow', () => {
|
|
it('publishes immediately with GitHub-generated notes', () => {
|
|
const step = getStep(releaseWorkflow, 'Create GitHub Release and Tag');
|
|
|
|
expect(step).toContain('--notes-start-tag "${PREVIOUS_RELEASE_TAG}"');
|
|
expect(step).toContain('--generate-notes');
|
|
expect(step).toContain(
|
|
'git merge-base --is-ancestor "${PREVIOUS_RELEASE_TAG}" HEAD',
|
|
);
|
|
expect(step).toContain('NOTES_START_TAG_FLAG=()');
|
|
expect(step).toContain(
|
|
'echo "::warning::PREVIOUS_RELEASE_TAG (${PREVIOUS_RELEASE_TAG}) is not an ancestor of HEAD; omitting --notes-start-tag"',
|
|
);
|
|
expect(step).toContain('"${NOTES_START_TAG_FLAG[@]}"');
|
|
expect(step).toContain("GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'");
|
|
expect(releaseWorkflow).not.toContain(
|
|
"name: 'Generate AI-assisted stable release notes'",
|
|
);
|
|
expect(releaseWorkflow).not.toContain("name: 'Regenerate CHANGELOG.md'");
|
|
expect(releaseWorkflow).not.toContain(
|
|
"name: 'Create PR to merge release branch into main'",
|
|
);
|
|
});
|
|
|
|
it('finalizes stable releases asynchronously', () => {
|
|
const validate = getStep(finalizeWorkflow, 'Validate stable release tag');
|
|
const generate = getStep(
|
|
finalizeWorkflow,
|
|
'Generate AI-assisted release notes',
|
|
);
|
|
const update = getStep(finalizeWorkflow, 'Update GitHub Release notes');
|
|
const changelog = getStep(finalizeWorkflow, 'Regenerate CHANGELOG.md');
|
|
|
|
expect(finalizeWorkflow).toContain("types: ['published']");
|
|
expect(finalizeWorkflow).toContain(
|
|
'github.event.release.prerelease == false',
|
|
);
|
|
expect(finalizeWorkflow).toContain('workflow_dispatch:');
|
|
expect(finalizeWorkflow).toContain(
|
|
'if [[ "${TAG}" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+$ ]]',
|
|
);
|
|
expect(validate).toContain('is not a stable release tag');
|
|
expect(validate).toContain('exit 1');
|
|
expect(generate).toContain('timeout-minutes: 35');
|
|
expect(generate).toContain('continue-on-error: true');
|
|
|
|
// The step timeout must exceed the script's internal budget; otherwise
|
|
// the runner SIGKILLs the step and even the fallback notes are lost.
|
|
const stepTimeoutMin = Number(
|
|
generate.match(/timeout-minutes:\s*(\d+)/)[1],
|
|
);
|
|
const budgetMs = releaseNotesScript
|
|
.match(/totalTimeoutMs\s*=\s*([\d_]+)\s*\*\s*([\d_]+)/)
|
|
.slice(1)
|
|
.map((part) => Number(part.replace(/_/g, '')))
|
|
.reduce((a, b) => a * b, 1);
|
|
expect(stepTimeoutMin * 60_000).toBeGreaterThan(budgetMs);
|
|
|
|
expect(generate).toContain('GitHub-generated notes');
|
|
expect(generate).toContain('node scripts/generate-release-notes.js');
|
|
expect(update).toContain('continue-on-error: true');
|
|
expect(update).toContain(
|
|
'gh release edit "${RELEASE_TAG}" --notes-file "${RELEASE_NOTES_FILE}"',
|
|
);
|
|
expect(changelog).not.toContain('continue-on-error: true');
|
|
});
|
|
|
|
it('updates the changelog before opening the release PR', () => {
|
|
const changelog = finalizeWorkflow.indexOf(
|
|
"name: 'Regenerate CHANGELOG.md'",
|
|
);
|
|
const pr = finalizeWorkflow.indexOf(
|
|
"name: 'Create PR to merge release branch into main'",
|
|
);
|
|
|
|
expect(changelog).toBeGreaterThanOrEqual(0);
|
|
expect(pr).toBeGreaterThan(changelog);
|
|
expect(finalizeWorkflow).toContain("name: 'Approve release PR'");
|
|
expect(finalizeWorkflow).toContain(
|
|
"name: 'Enable auto-merge for release PR'",
|
|
);
|
|
});
|
|
|
|
it('comments released-in version only for squash-merge PR trailers', () => {
|
|
const step = getStep(
|
|
finalizeWorkflow,
|
|
'Comment released-in version on merged PRs',
|
|
);
|
|
|
|
expect(step).toContain('continue-on-error: true');
|
|
expect(step).toContain("grep -oE '\\(#[0-9]+\\)$'");
|
|
expect(step).toContain("tr -d '()#'");
|
|
expect(step).not.toContain("grep -oE '#[0-9]+'");
|
|
expect(step).toContain("marker='<!-- qwen-release-comment:v1 -->'");
|
|
expect(step).toContain('gh pr view "${num}" --json comments');
|
|
expect(step).toContain('grep -qF "${marker}" <<<"${existing}"');
|
|
expect(step).toContain('gh pr comment "${num}" --body "${body}"');
|
|
});
|
|
|
|
it('does not recreate an already merged release PR during retries', () => {
|
|
const pr = getStep(
|
|
finalizeWorkflow,
|
|
'Create PR to merge release branch into main',
|
|
);
|
|
const approve = getStep(finalizeWorkflow, 'Approve release PR');
|
|
const merge = getStep(finalizeWorkflow, 'Enable auto-merge for release PR');
|
|
|
|
expect(pr).toContain('--state all');
|
|
expect(pr).toContain('select(.state == "MERGED")');
|
|
expect(pr).toContain('if [[ "${pr_state}" == "MERGED" ]]');
|
|
expect(pr).toContain('SHOULD_MERGE=false');
|
|
expect(approve).toContain("steps.pr.outputs.SHOULD_MERGE == 'true'");
|
|
expect(merge).toContain("steps.pr.outputs.SHOULD_MERGE == 'true'");
|
|
});
|
|
});
|