mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-31 20:04:41 +00:00
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.
258 lines
10 KiB
YAML
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]"
|