qwen-code/scripts/tests/ai-release-notes-workflow.test.js
易良 ff5c639e26
fix(release): raise model timeouts and shrink batch size for slow networks (#8007)
* 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
2026-07-29 22:38:35 +00:00

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'");
});
});