/** * @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=''"); 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'"); }); });