mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-12 14:10:08 +00:00
* feat(sdk-python): add pypi release workflow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): build cli before smoke test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten release conflict handling Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): harden python release workflow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten stable release guards Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): harden prerelease publish flow Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): reuse release branches on rerun Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): resume incomplete releases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(release): tighten missing-release checks Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): resume stable release reruns Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): tighten release recovery guards Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(sdk-python): cover release version edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address release workflow review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * refactor(sdk-python): address review feedback on release version script - Remove unreachable `if (type === 'stable')` branch in bumpVersion(); the stable path was dead code since getVersion() throws for all stable conflicts before calling bumpVersion(). Move nightly conflict throw to the call site for symmetry. - Rename getNextPatchBaseVersion → getNextBaseVersion to reflect that the function can return a prerelease base without incrementing patch. - Add test for preview+nightly coexistence where nightly base is higher. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): address remaining review feedback on release workflow - Fix failure-issue gate to read github.event.inputs.dry_run directly instead of steps.vars.outputs.is_dry_run (which is empty when early steps fail). Add --repo flag for gh issue create when checkout failed. - Add diagnostic state table to failure-issue body (RELEASE_TAG, PACKAGE_VERSION, PUBLISH_CHANNEL, RESUME_EXISTING_RELEASE, etc.) - Fix release-notes error swallow: only silence release not found / Not Found / HTTP 404, emit :⚠️: for other gh release view errors. - Improve validateVersion error messages to use human-readable format keys (X.Y.Z, X.Y.Z-preview.N) matching TS sibling convention. - Filter fully-yanked versions in getAllVersionsFromPyPI. - Add console.error log when stable is derived from nightly. - Add bash regex guard for inputs.version to prevent shell injection. - Use per-release-type concurrency groups (nightly/preview/stable). - Add jq null-guard checks for all 6 field extractions. - Remove misleading --follow-tags from git push (lightweight tags). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): rename misleading test description The test asserts that preview/nightly releases return empty previousReleaseTag, but the name said "same-channel previous release tags" which implied non-empty values. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): address unresolved review comments on release workflow - Remove -z check in extract_field() that blocked preview/nightly releases (previousReleaseTag is legitimately empty for non-stable releases) - Use static environment.url since step outputs aren't available at job startup - Use skip-existing for resumed PyPI publish to fill in missing artifacts - Add AbortSignal.timeout(30s) to PyPI fetch to prevent indefinite hangs - Add downgrade guard for stable_version_override - Use GHA :⚠️: annotation instead of console.error for visibility - Separate yanked/non-yanked version lists so conflict detection includes yanked versions (PyPI still reserves those slots) - Filter current release from previousReleaseTag to avoid self-reference on resume - Add tests for yanked conflict detection, downgrade guard, and resume previousReleaseTag Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address final review round on release version script - Fix getNextBaseVersion() first-release skip: use pyproject.toml version directly when PyPI has no stable versions instead of unconditionally incrementing - Fix getNextBaseVersion() off-by-one: change > to >= so equal prerelease base continues the existing line instead of incrementing patch - Add :⚠️: annotation when preview auto-bumps due to orphan git tags (tag exists without PyPI version or GitHub release) - Add set -euo pipefail to 5 workflow steps missing it: release_branch, persist_source, Create GitHub release, Delete prerelease branch, Create issue on failure - Fix 2 existing tests affected by first-release change, add 4 new tests 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(sdk-python): use stderr for GHA warning annotations to avoid corrupting JSON stdout console.log writes to stdout, which gets captured by VERSION_JSON=$(node ...) in the workflow and corrupts the JSON output for jq. Switch to console.error so :⚠️: annotations go to stderr (GHA recognizes workflow commands on both streams). Also add set -euo pipefail to the "Get the version" step for consistency with other workflow steps. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
515 lines
21 KiB
YAML
515 lines
21 KiB
YAML
name: 'Release Python SDK'
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'The version to release (e.g., v0.1.0 for stable, v0.1.1-preview.0 for preview).'
|
|
required: false
|
|
type: 'string'
|
|
ref:
|
|
description: 'The protected branch ref to release SDK from. This privileged workflow only permits main.'
|
|
required: true
|
|
type: 'string'
|
|
default: 'main'
|
|
dry_run:
|
|
description: 'Run the release flow without publishing to PyPI or creating persistent release branches/tags.'
|
|
required: true
|
|
type: 'boolean'
|
|
default: true
|
|
create_nightly_release:
|
|
description: 'Auto-apply the nightly release tag. Input version is ignored.'
|
|
required: false
|
|
type: 'boolean'
|
|
default: false
|
|
create_preview_release:
|
|
description: 'Auto-apply the preview release tag. Input version is ignored unless explicitly provided.'
|
|
required: false
|
|
type: 'boolean'
|
|
default: false
|
|
force_skip_tests:
|
|
description: 'Skip Python checks and smoke test. Production releases should keep this disabled.'
|
|
required: false
|
|
type: 'boolean'
|
|
default: false
|
|
|
|
concurrency:
|
|
group: '${{ github.workflow }}-${{ github.event.inputs.create_nightly_release == ''true'' && ''nightly'' || github.event.inputs.create_preview_release == ''true'' && ''preview'' || ''stable'' }}'
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
release-sdk-python:
|
|
runs-on: 'ubuntu-latest'
|
|
environment:
|
|
name: 'production-release'
|
|
url: '${{ github.server_url }}/${{ github.repository }}/releases'
|
|
if: |-
|
|
${{ github.repository == 'QwenLM/qwen-code' }}
|
|
permissions:
|
|
contents: 'write'
|
|
id-token: 'write'
|
|
issues: 'write'
|
|
pull-requests: 'write'
|
|
|
|
steps:
|
|
- name: 'Validate release inputs'
|
|
env:
|
|
CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}'
|
|
CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}'
|
|
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
|
|
FORCE_SKIP_TESTS: '${{ github.event.inputs.force_skip_tests }}'
|
|
MANUAL_VERSION: '${{ inputs.version }}'
|
|
REQUESTED_REF: '${{ github.event.inputs.ref || github.sha }}'
|
|
WORKFLOW_REF: '${{ github.ref }}'
|
|
run: |
|
|
if [[ "${CREATE_NIGHTLY_RELEASE}" == "true" && "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then
|
|
echo "create_nightly_release and create_preview_release cannot both be true" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "${MANUAL_VERSION}" ]]; then
|
|
if [[ ! "${MANUAL_VERSION}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-preview\.[0-9]+)?$ ]]; then
|
|
echo "Invalid version format: ${MANUAL_VERSION}" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
case "${WORKFLOW_REF}" in
|
|
refs/heads/main)
|
|
;;
|
|
*)
|
|
echo "This privileged workflow must be launched from the protected main workflow branch." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REQUESTED_REF}" in
|
|
main|refs/heads/main)
|
|
;;
|
|
*)
|
|
echo "This privileged workflow must use the protected main ref." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [[ "${DRY_RUN_INPUT}" != "true" ]]; then
|
|
if [[ "${FORCE_SKIP_TESTS}" == "true" ]]; then
|
|
echo "force_skip_tests cannot be used when dry_run is false" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
|
with:
|
|
ref: '${{ github.event.inputs.ref || github.sha }}'
|
|
fetch-depth: 0
|
|
|
|
- name: 'Set booleans for simplified logic'
|
|
env:
|
|
CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}'
|
|
CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}'
|
|
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
|
|
id: 'vars'
|
|
run: |
|
|
is_nightly="false"
|
|
if [[ "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then
|
|
is_nightly="true"
|
|
fi
|
|
echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}"
|
|
|
|
is_preview="false"
|
|
if [[ "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then
|
|
is_preview="true"
|
|
fi
|
|
echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}"
|
|
|
|
is_dry_run="false"
|
|
if [[ "${DRY_RUN_INPUT}" == "true" ]]; then
|
|
is_dry_run="true"
|
|
fi
|
|
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: 'Capture checked-out commit'
|
|
id: 'checkout_sha'
|
|
run: |
|
|
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: 'Setup Node.js'
|
|
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
|
|
- name: 'Get the version'
|
|
id: 'version'
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION_ARGS=()
|
|
if [[ "${IS_NIGHTLY}" == "true" ]]; then
|
|
VERSION_ARGS+=(--type=nightly)
|
|
elif [[ "${IS_PREVIEW}" == "true" ]]; then
|
|
VERSION_ARGS+=(--type=preview)
|
|
if [[ -n "${MANUAL_VERSION}" ]]; then
|
|
VERSION_ARGS+=("--preview_version_override=${MANUAL_VERSION}")
|
|
fi
|
|
else
|
|
VERSION_ARGS+=(--type=stable)
|
|
if [[ -n "${MANUAL_VERSION}" ]]; then
|
|
VERSION_ARGS+=("--stable_version_override=${MANUAL_VERSION}")
|
|
fi
|
|
fi
|
|
|
|
VERSION_JSON=$(node packages/sdk-python/scripts/get-release-version.js "${VERSION_ARGS[@]}")
|
|
|
|
extract_field() {
|
|
local value
|
|
value=$(echo "$VERSION_JSON" | jq -r ".$1")
|
|
if [[ "${value}" == "null" ]]; then
|
|
echo "Failed to extract $1 from version JSON: ${VERSION_JSON}" >&2
|
|
exit 1
|
|
fi
|
|
echo "${value}"
|
|
}
|
|
|
|
RELEASE_TAG=$(extract_field releaseTag)
|
|
RELEASE_VERSION=$(extract_field releaseVersion)
|
|
PACKAGE_VERSION=$(extract_field packageVersion)
|
|
PUBLISH_CHANNEL=$(extract_field publishChannel)
|
|
PREVIOUS_RELEASE_TAG=$(extract_field previousReleaseTag)
|
|
RESUME_EXISTING_RELEASE=$(extract_field resumeExistingRelease)
|
|
|
|
echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "PUBLISH_CHANNEL=${PUBLISH_CHANNEL}" >> "$GITHUB_OUTPUT"
|
|
echo "PREVIOUS_RELEASE_TAG=${PREVIOUS_RELEASE_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "RESUME_EXISTING_RELEASE=${RESUME_EXISTING_RELEASE}" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "========================================"
|
|
echo "Python SDK Release Version Info"
|
|
echo "========================================"
|
|
echo "Release Tag: ${RELEASE_TAG}"
|
|
echo "Release Version: ${RELEASE_VERSION}"
|
|
echo "Package Version: ${PACKAGE_VERSION}"
|
|
echo "Publish Channel: ${PUBLISH_CHANNEL}"
|
|
echo "Previous Release: ${PREVIOUS_RELEASE_TAG}"
|
|
echo "Resume Existing: ${RESUME_EXISTING_RELEASE}"
|
|
echo "========================================"
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}'
|
|
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
|
MANUAL_VERSION: '${{ inputs.version }}'
|
|
|
|
- name: 'Setup Python'
|
|
uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5
|
|
with:
|
|
# Keep in sync with packages/sdk-python/pyproject.toml [project] requires-python.
|
|
python-version: '3.11'
|
|
|
|
- name: 'Install Dependencies'
|
|
run: |
|
|
npm ci
|
|
python -m pip install --upgrade pip
|
|
python -m pip install -e 'packages/sdk-python[dev]' build
|
|
|
|
- name: 'Build qwen CLI bundle'
|
|
if: |-
|
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
|
run: |
|
|
npm run build
|
|
npm run bundle
|
|
|
|
- name: 'Set Python package version (local only)'
|
|
env:
|
|
PACKAGE_VERSION: '${{ steps.version.outputs.PACKAGE_VERSION }}'
|
|
run: |
|
|
python - <<'PY'
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
|
|
pyproject = Path('packages/sdk-python/pyproject.toml')
|
|
content = pyproject.read_text()
|
|
updated, count = re.subn(
|
|
r'^version = "[^"]+"$',
|
|
f'version = "{os.environ["PACKAGE_VERSION"]}"',
|
|
content,
|
|
flags=re.MULTILINE,
|
|
)
|
|
if count != 1:
|
|
raise SystemExit(
|
|
f'pyproject.toml version rewrite matched {count} lines, expected 1'
|
|
)
|
|
pyproject.write_text(updated)
|
|
PY
|
|
|
|
- name: 'Run Python quality gates'
|
|
if: |-
|
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
|
run: |
|
|
python -m ruff check --config packages/sdk-python/pyproject.toml packages/sdk-python
|
|
python -m ruff format --check --config packages/sdk-python/pyproject.toml packages/sdk-python
|
|
python -m mypy --config-file packages/sdk-python/pyproject.toml packages/sdk-python/src
|
|
python -m pytest -c packages/sdk-python/pyproject.toml packages/sdk-python/tests -q
|
|
|
|
- name: 'Run real smoke test'
|
|
if: |-
|
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
|
env:
|
|
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
|
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
|
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
|
run: |
|
|
python packages/sdk-python/scripts/smoke_real.py --qwen "${GITHUB_WORKSPACE}/dist/cli.js" --cwd "${GITHUB_WORKSPACE}" --json-only
|
|
|
|
- name: 'Configure Git User'
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: 'Create and switch to a release branch'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
|
id: 'release_branch'
|
|
env:
|
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
|
RESUME_EXISTING_RELEASE: '${{ steps.version.outputs.RESUME_EXISTING_RELEASE }}'
|
|
run: |
|
|
set -euo pipefail
|
|
BRANCH_NAME="release/sdk-python/${RELEASE_TAG}"
|
|
VERSIONED_PYPROJECT="$(mktemp)"
|
|
cp packages/sdk-python/pyproject.toml "${VERSIONED_PYPROJECT}"
|
|
git restore --staged --worktree packages/sdk-python/pyproject.toml
|
|
|
|
if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then
|
|
git fetch origin "${BRANCH_NAME}"
|
|
if git show-ref --verify --quiet "refs/heads/${BRANCH_NAME}"; then
|
|
git switch "${BRANCH_NAME}"
|
|
else
|
|
git switch -c "${BRANCH_NAME}" --track "origin/${BRANCH_NAME}"
|
|
fi
|
|
git reset --hard "origin/${BRANCH_NAME}"
|
|
echo "REMOTE_BRANCH_EXISTS=true" >> "${GITHUB_OUTPUT}"
|
|
else
|
|
if [[ "${RESUME_EXISTING_RELEASE}" == "true" ]]; then
|
|
echo "Published version ${RELEASE_TAG} exists without a persisted release branch ${BRANCH_NAME}. To resolve: (1) re-trigger with a different version, or (2) manually create branch ${BRANCH_NAME} from the original release commit." >&2
|
|
exit 1
|
|
fi
|
|
git switch -c "${BRANCH_NAME}"
|
|
echo "REMOTE_BRANCH_EXISTS=false" >> "${GITHUB_OUTPUT}"
|
|
fi
|
|
|
|
cp "${VERSIONED_PYPROJECT}" packages/sdk-python/pyproject.toml
|
|
rm -f "${VERSIONED_PYPROJECT}"
|
|
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: 'Commit and push package version'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
|
id: 'persist_source'
|
|
env:
|
|
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
|
CHECKED_OUT_SHA: '${{ steps.checkout_sha.outputs.sha }}'
|
|
REMOTE_BRANCH_EXISTS: '${{ steps.release_branch.outputs.REMOTE_BRANCH_EXISTS }}'
|
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
|
run: |
|
|
set -euo pipefail
|
|
git add packages/sdk-python/pyproject.toml
|
|
if git diff --staged --quiet; then
|
|
echo "No version changes to commit"
|
|
if [[ "${REMOTE_BRANCH_EXISTS}" == "true" ]]; then
|
|
echo "HAS_PERSISTED_SOURCE=true" >> "${GITHUB_OUTPUT}"
|
|
echo "RELEASE_TARGET_SHA=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
|
|
else
|
|
echo "HAS_PERSISTED_SOURCE=false" >> "${GITHUB_OUTPUT}"
|
|
echo "RELEASE_TARGET_SHA=${CHECKED_OUT_SHA}" >> "${GITHUB_OUTPUT}"
|
|
fi
|
|
else
|
|
git commit -m "chore(release): sdk-python ${RELEASE_TAG}"
|
|
echo "HAS_PERSISTED_SOURCE=true" >> "${GITHUB_OUTPUT}"
|
|
echo "RELEASE_TARGET_SHA=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
|
|
if [[ "${REMOTE_BRANCH_EXISTS}" == "true" ]]; then
|
|
git push origin "${BRANCH_NAME}"
|
|
else
|
|
git push --set-upstream origin "${BRANCH_NAME}"
|
|
fi
|
|
fi
|
|
|
|
- name: 'Build Python package'
|
|
working-directory: 'packages/sdk-python'
|
|
run: |
|
|
rm -rf dist
|
|
python -m build
|
|
|
|
- name: 'Publish qwen-code-sdk to PyPI'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
|
uses: 'pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b' # ratchet:pypa/gh-action-pypi-publish@release/v1
|
|
with:
|
|
packages-dir: 'packages/sdk-python/dist'
|
|
# skip-existing handles resumed releases where some artifacts were already uploaded
|
|
skip-existing: '${{ steps.version.outputs.RESUME_EXISTING_RELEASE }}'
|
|
|
|
- name: 'Show publish artifacts for dry-run'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'true' }}
|
|
run: |
|
|
ls -la packages/sdk-python/dist
|
|
|
|
- name: 'Create GitHub release and tag'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
|
PACKAGE_VERSION: '${{ steps.version.outputs.PACKAGE_VERSION }}'
|
|
PREVIOUS_RELEASE_TAG: '${{ steps.version.outputs.PREVIOUS_RELEASE_TAG }}'
|
|
RELEASE_TARGET_SHA: '${{ steps.persist_source.outputs.RELEASE_TARGET_SHA }}'
|
|
IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}'
|
|
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TAG_NAME="sdk-python-${RELEASE_TAG}"
|
|
|
|
if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then
|
|
PRERELEASE_FLAG="--prerelease"
|
|
else
|
|
PRERELEASE_FLAG=""
|
|
fi
|
|
|
|
if gh release view "${TAG_NAME}" --json tagName >/dev/null 2>&1; then
|
|
echo "::warning::GitHub release ${TAG_NAME} already exists; skipping create."
|
|
exit 0
|
|
fi
|
|
|
|
if git rev-parse "${TAG_NAME}" >/dev/null 2>&1; then
|
|
EXISTING_TAG_SHA="$(git rev-list -n 1 "${TAG_NAME}")"
|
|
if [[ "${EXISTING_TAG_SHA}" != "${RELEASE_TARGET_SHA}" ]]; then
|
|
echo "Existing tag ${TAG_NAME} points to ${EXISTING_TAG_SHA}, expected ${RELEASE_TARGET_SHA}." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
NOTES_FILE=$(mktemp)
|
|
{
|
|
echo "## Published Package"
|
|
echo ""
|
|
echo "- PyPI package: \`qwen-code-sdk\`"
|
|
echo "- Package version: \`${PACKAGE_VERSION}\`"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
} > "${NOTES_FILE}"
|
|
|
|
if [[ -n "${PREVIOUS_RELEASE_TAG}" ]]; then
|
|
PREVIOUS_NOTES=$(gh release view "sdk-python-${PREVIOUS_RELEASE_TAG}" --json body -q '.body' 2>&1) || {
|
|
ERR_MSG="${PREVIOUS_NOTES}"
|
|
case "${ERR_MSG}" in
|
|
*"release not found"*|*"Not Found"*|*"HTTP 404"*)
|
|
PREVIOUS_NOTES='See commit history for changes.'
|
|
;;
|
|
*)
|
|
echo "::warning::Failed to fetch previous release notes: ${ERR_MSG}"
|
|
PREVIOUS_NOTES='See commit history for changes.'
|
|
;;
|
|
esac
|
|
}
|
|
printf '%s\n' "${PREVIOUS_NOTES}" >> "${NOTES_FILE}"
|
|
else
|
|
echo "See commit history for changes." >> "${NOTES_FILE}"
|
|
fi
|
|
|
|
gh release create "${TAG_NAME}" \
|
|
--target "${RELEASE_TARGET_SHA}" \
|
|
--title "SDK Python Release ${RELEASE_TAG}" \
|
|
--notes-file "${NOTES_FILE}" \
|
|
${PRERELEASE_FLAG}
|
|
|
|
rm -f "${NOTES_FILE}"
|
|
|
|
- name: 'Delete prerelease release branch'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' && (steps.vars.outputs.is_nightly == 'true' || steps.vars.outputs.is_preview == 'true') }}
|
|
env:
|
|
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
|
run: |
|
|
set -euo pipefail
|
|
if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then
|
|
if ! git push origin --delete "${BRANCH_NAME}"; then
|
|
echo "::warning::Failed to delete prerelease release branch ${BRANCH_NAME}; release already exists, so remove the branch manually if it still exists."
|
|
fi
|
|
else
|
|
echo "No prerelease release branch to delete for ${BRANCH_NAME}."
|
|
fi
|
|
|
|
- name: 'Create PR to merge release branch into main'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' && steps.persist_source.outputs.HAS_PERSISTED_SOURCE == 'true' }}
|
|
id: 'pr'
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
|
|
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
pr_url="$(gh pr list --head "${RELEASE_BRANCH}" --base main --json url --jq '.[0].url')"
|
|
if [[ -z "${pr_url}" ]]; then
|
|
pr_url="$(gh pr create \
|
|
--base main \
|
|
--head "${RELEASE_BRANCH}" \
|
|
--title "chore(release): sdk-python ${RELEASE_TAG}" \
|
|
--body "Automated release PR for sdk-python ${RELEASE_TAG}.")"
|
|
fi
|
|
|
|
echo "PR_URL=${pr_url}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: 'Enable auto-merge for release PR'
|
|
if: |-
|
|
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' && steps.persist_source.outputs.HAS_PERSISTED_SOURCE == 'true' }}
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
|
|
PR_URL: '${{ steps.pr.outputs.PR_URL }}'
|
|
run: |
|
|
set -euo pipefail
|
|
gh pr merge "${PR_URL}" --merge --auto --delete-branch
|
|
|
|
- name: 'Create issue on failure'
|
|
if: |-
|
|
${{ failure() && github.event.inputs.dry_run != 'true' }}
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
RELEASE_TAG: "${{ steps.version.outputs.RELEASE_TAG || 'N/A' }}"
|
|
PACKAGE_VERSION: "${{ steps.version.outputs.PACKAGE_VERSION || 'N/A' }}"
|
|
PUBLISH_CHANNEL: "${{ steps.version.outputs.PUBLISH_CHANNEL || 'N/A' }}"
|
|
RESUME_EXISTING_RELEASE: "${{ steps.version.outputs.RESUME_EXISTING_RELEASE || 'N/A' }}"
|
|
BRANCH_NAME: "${{ steps.release_branch.outputs.BRANCH_NAME || 'N/A' }}"
|
|
HAS_PERSISTED_SOURCE: "${{ steps.persist_source.outputs.HAS_PERSISTED_SOURCE || 'N/A' }}"
|
|
RELEASE_TARGET_SHA: "${{ steps.persist_source.outputs.RELEASE_TARGET_SHA || 'N/A' }}"
|
|
PR_URL: "${{ steps.pr.outputs.PR_URL || 'N/A' }}"
|
|
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
|
run: |
|
|
set -euo pipefail
|
|
BODY=$(cat <<BODY
|
|
The Python SDK release workflow failed.
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Release Tag | \`${RELEASE_TAG}\` |
|
|
| Package Version | \`${PACKAGE_VERSION}\` |
|
|
| Publish Channel | \`${PUBLISH_CHANNEL}\` |
|
|
| Resume Existing | \`${RESUME_EXISTING_RELEASE}\` |
|
|
| Release Branch | \`${BRANCH_NAME}\` |
|
|
| Persisted Source | \`${HAS_PERSISTED_SOURCE}\` |
|
|
| Target SHA | \`${RELEASE_TARGET_SHA}\` |
|
|
| PR URL | ${PR_URL} |
|
|
|
|
See the full run for details: ${DETAILS_URL}
|
|
BODY
|
|
)
|
|
gh issue create \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--title "Python SDK release failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
|
|
--body "${BODY}"
|