qwen-code/.github/workflows/finalize-release.yml
易良 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

258 lines
10 KiB
YAML

name: 'Finalize Stable Release'
on:
release:
types: ['published']
workflow_dispatch:
inputs:
tag:
description: 'The stable release tag to finalize (e.g., v0.19.10).'
required: true
type: 'string'
concurrency:
group: 'finalize-stable-release-${{ github.event.release.tag_name || inputs.tag }}'
cancel-in-progress: false
jobs:
finalize:
name: 'Finalize Stable Release'
runs-on: 'ubuntu-latest'
if: |-
${{ github.repository == 'QwenLM/qwen-code' && (github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false) }}
environment:
name: 'production-release'
permissions:
contents: 'read'
env:
RELEASE_TAG: '${{ github.event.release.tag_name || inputs.tag }}'
steps:
- name: 'Validate stable release tag'
id: 'meta'
env:
TAG: '${{ env.RELEASE_TAG }}'
run: |-
is_stable="false"
if [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
is_stable="true"
fi
echo "is_stable=${is_stable}" >> "${GITHUB_OUTPUT}"
echo "release_branch=release/${TAG}" >> "${GITHUB_OUTPUT}"
if [[ "${is_stable}" != "true" ]]; then
echo "::error::${TAG} is not a stable release tag; finalize handles stable tags only."
exit 1
fi
- name: 'Checkout release branch'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
token: '${{ secrets.CI_BOT_PAT }}'
ref: '${{ steps.meta.outputs.release_branch }}'
fetch-depth: 0
- name: 'Setup Node.js'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
- name: 'Install Dependencies'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
env:
NPM_CONFIG_PREFER_OFFLINE: 'true'
QWEN_SKIP_PREPARE: '1'
run: |-
npm ci --no-audit --progress=false
- name: 'Configure Git User'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
run: |-
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config core.hooksPath .husky
- name: 'Resolve previous stable release tag'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
id: 'previous'
env:
TAG: '${{ env.RELEASE_TAG }}'
run: |-
previous_tag=""
found_current="false"
while IFS= read -r stable_tag; do
if [[ "${found_current}" == "true" ]]; then
previous_tag="${stable_tag}"
break
fi
if [[ "${stable_tag}" == "${TAG}" ]]; then
found_current="true"
fi
done < <(git tag --list 'v*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$')
if [[ -z "${previous_tag}" ]]; then
echo "::error::Could not resolve the stable release before ${TAG}."
exit 1
fi
echo "tag=${previous_tag}" >> "${GITHUB_OUTPUT}"
- name: 'Generate AI-assisted release notes'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
continue-on-error: true
timeout-minutes: 35
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
PREVIOUS_RELEASE_TAG: '${{ steps.previous.outputs.tag }}'
RELEASE_BRANCH: '${{ steps.meta.outputs.release_branch }}'
RELEASE_NOTES_FILE: '${{ runner.temp }}/release-notes.md'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
# The first publish keeps GitHub-generated notes; this improves them after the tag exists.
node scripts/generate-release-notes.js \
--repo="${GITHUB_REPOSITORY}" \
--tag="${RELEASE_TAG}" \
--previous-tag="${PREVIOUS_RELEASE_TAG}" \
--target="${RELEASE_BRANCH}" \
--output="${RELEASE_NOTES_FILE}"
- name: 'Update GitHub Release notes'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
continue-on-error: true
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
RELEASE_NOTES_FILE: '${{ runner.temp }}/release-notes.md'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
if [[ -s "${RELEASE_NOTES_FILE}" ]]; then
gh release edit "${RELEASE_TAG}" --notes-file "${RELEASE_NOTES_FILE}"
else
echo "::warning::AI release notes were not generated; keeping GitHub-generated notes."
fi
- name: 'Comment released-in version on merged PRs'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
continue-on-error: true
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
PREVIOUS_TAG: '${{ steps.previous.outputs.tag }}'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
set -euo pipefail
# Squash-merge commit subjects carry the PR as "(#NNN)"; collect those between the two tags.
# The trailing "|| true" keeps "no matches" (empty range) from failing under pipefail.
pr_numbers="$(git log --pretty=format:'%s' "${PREVIOUS_TAG}..${RELEASE_TAG}" | grep -oE '\(#[0-9]+\)$' | tr -d '()#' | sort -un || true)"
if [[ -z "${pr_numbers}" ]]; then
echo "No PR references found between ${PREVIOUS_TAG} and ${RELEASE_TAG}."
exit 0
fi
echo "Found PRs between ${PREVIOUS_TAG} and ${RELEASE_TAG}:"
echo "${pr_numbers}"
marker='<!-- qwen-release-comment:v1 -->'
release_url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}"
while IFS= read -r num; do
if [[ -z "${num}" ]]; then
continue
fi
# Re-run safety: skip PRs that already carry this release marker.
existing="$(gh pr view "${num}" --json comments --jq '.comments[].body' 2>/dev/null || true)"
if grep -qF "${marker}" <<<"${existing}"; then
echo "PR #${num} already has a release comment; skipping."
continue
fi
body="${marker}"$'\n'"Released in [${RELEASE_TAG}](${release_url})."
gh pr comment "${num}" --body "${body}" && echo "Commented on PR #${num}." || echo "::warning::Failed to comment on PR #${num}."
done <<< "${pr_numbers}"
- name: 'Regenerate CHANGELOG.md'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
BRANCH_NAME: '${{ steps.meta.outputs.release_branch }}'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
set -euo pipefail
node scripts/generate-changelog.js
git add CHANGELOG.md
if git diff --cached --quiet -- CHANGELOG.md; then
echo "CHANGELOG.md already up to date."
else
git commit -m "docs(changelog): sync for ${RELEASE_TAG}"
git push origin "${BRANCH_NAME}"
fi
- name: 'Create PR to merge release branch into main'
if: |-
${{ steps.meta.outputs.is_stable == 'true' }}
id: 'pr'
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
RELEASE_BRANCH: '${{ steps.meta.outputs.release_branch }}'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
set -euo pipefail
# Merge the release branch back to main so package metadata and CHANGELOG converge.
pr_data="$(gh pr list --head "${RELEASE_BRANCH}" --base main --state all --json url,state --jq '(map(select(.state == "MERGED"))[0] // map(select(.state == "OPEN"))[0]) // empty | [.url, .state] | @tsv')"
pr_url=""
pr_state=""
if [[ -n "${pr_data}" ]]; then
IFS=$'\t' read -r pr_url pr_state <<< "${pr_data}"
fi
if [[ "${pr_state}" == "MERGED" ]]; then
echo "Release PR already merged; skipping approval and auto-merge."
echo "PR_URL=${pr_url}" >> "${GITHUB_OUTPUT}"
echo "SHOULD_MERGE=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
if [[ "${pr_state}" != "OPEN" ]]; then
pr_url="$(gh pr create \
--base main \
--head "${RELEASE_BRANCH}" \
--label 'skip-changelog' \
--title "chore(release): ${RELEASE_TAG}" \
--body "Automated release PR for ${RELEASE_TAG}. Syncs package.json versions and CHANGELOG.md on main.")"
fi
echo "PR_URL=${pr_url}" >> "${GITHUB_OUTPUT}"
echo "SHOULD_MERGE=true" >> "${GITHUB_OUTPUT}"
- name: 'Approve release PR'
if: |-
${{ steps.meta.outputs.is_stable == 'true' && steps.pr.outputs.SHOULD_MERGE == 'true' }}
env:
GITHUB_TOKEN: '${{ secrets.CI_DEV_BOT_PAT }}'
PR_URL: '${{ steps.pr.outputs.PR_URL }}'
run: |-
set -euo pipefail
gh pr review "${PR_URL}" --approve --body "Automated approval for the release version bump."
- name: 'Enable auto-merge for release PR'
if: |-
${{ steps.meta.outputs.is_stable == 'true' && steps.pr.outputs.SHOULD_MERGE == 'true' }}
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
PR_URL: '${{ steps.pr.outputs.PR_URL }}'
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
run: |-
set -euo pipefail
gh pr merge "${PR_URL}" \
--squash \
--auto \
--subject "chore(release): ${RELEASE_TAG} [skip ci]"