Release #92
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 'Release' | |
| on: | |
| schedule: | |
| # Runs every day at midnight UTC for the nightly release. | |
| - cron: '0 0 * * *' | |
| # Runs every Tuesday at 23:59 UTC for the preview release. | |
| - cron: '59 23 * * 2' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'The version to release (e.g., v0.1.11 or v0.1.11-preview.0). Required for manual patch releases.' | |
| required: false | |
| type: 'string' | |
| ref: | |
| description: 'The branch or ref (full git sha) to release from.' | |
| required: true | |
| type: 'string' | |
| default: 'main' | |
| dry_run: | |
| description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.' | |
| 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: 'Create a preview release. If version is X.Y.Z-preview.N, use it as-is. If version is X.Y.Z, derive X.Y.Z-preview.0.' | |
| required: false | |
| type: 'boolean' | |
| default: false | |
| force_skip_tests: | |
| description: 'Skip the release validation jobs ("quality", "integration_none", and "integration_docker"), allowing publish to proceed without them. Prod releases should run validation.' | |
| required: false | |
| type: 'boolean' | |
| default: false | |
| jobs: | |
| prepare: | |
| name: 'Prepare Release Metadata' | |
| runs-on: 'ubuntu-latest' | |
| if: |- | |
| ${{ github.repository == 'QwenLM/qwen-code' }} | |
| permissions: | |
| contents: 'read' | |
| outputs: | |
| release_tag: '${{ steps.version.outputs.RELEASE_TAG }}' | |
| release_version: '${{ steps.version.outputs.RELEASE_VERSION }}' | |
| npm_tag: '${{ steps.version.outputs.NPM_TAG }}' | |
| previous_release_tag: '${{ steps.version.outputs.PREVIOUS_RELEASE_TAG }}' | |
| is_nightly: '${{ steps.vars.outputs.is_nightly }}' | |
| is_preview: '${{ steps.vars.outputs.is_preview }}' | |
| is_dry_run: '${{ steps.vars.outputs.is_dry_run }}' | |
| steps: | |
| - name: 'Checkout' | |
| uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 | |
| with: | |
| ref: '${{ github.event.inputs.ref || github.sha }}' | |
| fetch-depth: 0 | |
| - name: 'Set booleans for simplified logic' | |
| id: 'vars' | |
| env: | |
| CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}' | |
| CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}' | |
| CRON: '${{ github.event.schedule }}' | |
| DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}' | |
| run: |- | |
| is_nightly="false" | |
| if [[ "${CRON}" == "0 0 * * *" || "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then | |
| is_nightly="true" | |
| fi | |
| echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}" | |
| is_preview="false" | |
| if [[ "${CRON}" == "59 23 * * 2" || "${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: 'Setup Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'npm' | |
| cache-dependency-path: 'package-lock.json' | |
| - name: 'Install Dependencies' | |
| env: | |
| NPM_CONFIG_PREFER_OFFLINE: 'true' | |
| run: |- | |
| npm ci --no-audit --progress=false | |
| - name: 'Get the version' | |
| id: 'version' | |
| env: | |
| GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' | |
| IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' | |
| IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}' | |
| MANUAL_VERSION: '${{ inputs.version }}' | |
| run: |- | |
| 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 | |
| MANUAL_CLEAN="${MANUAL_VERSION#v}" | |
| if [[ "${MANUAL_CLEAN}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-preview\.[0-9]+$ ]]; then | |
| VERSION_ARGS+=("--preview_version_override=${MANUAL_CLEAN}") | |
| elif [[ "${MANUAL_CLEAN}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| VERSION_ARGS+=("--preview_version_override=${MANUAL_CLEAN}-preview.0") | |
| else | |
| echo "::error::For preview releases, version must be X.Y.Z or X.Y.Z-preview.N; got ${MANUAL_VERSION}" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| VERSION_ARGS+=(--type=stable) | |
| if [[ -n "${MANUAL_VERSION}" ]]; then | |
| VERSION_ARGS+=("--stable_version_override=${MANUAL_VERSION}") | |
| fi | |
| fi | |
| VERSION_JSON=$(node scripts/get-release-version.js "${VERSION_ARGS[@]}") | |
| echo "RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .releaseTag)" >> "$GITHUB_OUTPUT" | |
| echo "RELEASE_VERSION=$(echo "$VERSION_JSON" | jq -r .releaseVersion)" >> "$GITHUB_OUTPUT" | |
| echo "NPM_TAG=$(echo "$VERSION_JSON" | jq -r .npmTag)" >> "$GITHUB_OUTPUT" | |
| echo "PREVIOUS_RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .previousReleaseTag)" >> "$GITHUB_OUTPUT" | |
| quality: | |
| name: 'Quality Checks' | |
| runs-on: 'ubuntu-latest' | |
| needs: 'prepare' | |
| if: |- | |
| ${{ github.event.inputs.force_skip_tests != 'true' }} | |
| permissions: | |
| contents: 'read' | |
| env: | |
| OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' | |
| OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' | |
| OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' | |
| steps: | |
| - name: 'Checkout' | |
| uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 | |
| with: | |
| ref: '${{ github.event.inputs.ref || github.sha }}' | |
| fetch-depth: 0 | |
| - name: 'Setup Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'npm' | |
| cache-dependency-path: 'package-lock.json' | |
| - name: 'Install Dependencies' | |
| env: | |
| NPM_CONFIG_PREFER_OFFLINE: 'true' | |
| run: |- | |
| npm ci --no-audit --progress=false | |
| - name: 'Format Project' | |
| run: |- | |
| npm run format | |
| - name: 'Run Lint' | |
| run: |- | |
| npm run lint:ci | |
| - name: 'Build Project' | |
| run: |- | |
| npm run build | |
| - name: 'Typecheck Project' | |
| run: |- | |
| npm run typecheck | |
| - name: 'Run Workspace Tests' | |
| run: |- | |
| npm run test:ci | |
| integration_none: | |
| name: 'Integration Tests (No Sandbox)' | |
| runs-on: 'ubuntu-latest' | |
| needs: 'prepare' | |
| if: |- | |
| ${{ github.event.inputs.force_skip_tests != 'true' }} | |
| permissions: | |
| contents: 'read' | |
| env: | |
| OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' | |
| OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' | |
| OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' | |
| steps: | |
| - name: 'Checkout' | |
| uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 | |
| with: | |
| ref: '${{ github.event.inputs.ref || github.sha }}' | |
| fetch-depth: 0 | |
| - name: 'Setup Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'npm' | |
| cache-dependency-path: 'package-lock.json' | |
| - name: 'Install Dependencies' | |
| env: | |
| NPM_CONFIG_PREFER_OFFLINE: 'true' | |
| run: |- | |
| npm ci --no-audit --progress=false | |
| - name: 'Run CLI Integration Tests' | |
| run: |- | |
| npm run test:integration:cli:sandbox:none | |
| - name: 'Run Interactive Integration Tests' | |
| run: |- | |
| npm run test:integration:interactive:sandbox:none | |
| integration_docker: | |
| name: 'Integration Tests (Docker)' | |
| runs-on: 'ubuntu-latest' | |
| needs: 'prepare' | |
| if: |- | |
| ${{ github.event.inputs.force_skip_tests != 'true' }} | |
| permissions: | |
| contents: 'read' | |
| env: | |
| OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' | |
| OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' | |
| OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' | |
| steps: | |
| - name: 'Checkout' | |
| uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 | |
| with: | |
| ref: '${{ github.event.inputs.ref || github.sha }}' | |
| fetch-depth: 0 | |
| - name: 'Setup Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'npm' | |
| cache-dependency-path: 'package-lock.json' | |
| - name: 'Install Dependencies' | |
| env: | |
| NPM_CONFIG_PREFER_OFFLINE: 'true' | |
| run: |- | |
| npm ci --no-audit --progress=false | |
| - name: 'Set up Docker' | |
| uses: 'docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd' # ratchet:docker/setup-buildx-action@v4 | |
| - name: 'Build Sandbox' | |
| env: | |
| QWEN_SANDBOX: 'docker' | |
| run: |- | |
| npm run build:sandbox -- -s | |
| - name: 'Run CLI Docker Integration Tests' | |
| run: |- | |
| # The package.json docker test scripts each rebuild the sandbox image. | |
| # Run vitest directly here so this job reuses the image built above. | |
| QWEN_SANDBOX=docker npx vitest run --root ./integration-tests cli | |
| - name: 'Run Interactive Docker Integration Tests' | |
| run: |- | |
| QWEN_SANDBOX=docker npx vitest run --root ./integration-tests interactive | |
| publish: | |
| name: 'Publish Release' | |
| runs-on: 'ubuntu-latest' | |
| needs: | |
| - 'prepare' | |
| - 'quality' | |
| - 'integration_none' | |
| - 'integration_docker' | |
| if: |- | |
| ${{ | |
| always() && | |
| needs.prepare.result == 'success' && | |
| ( | |
| github.event.inputs.force_skip_tests == 'true' || | |
| ( | |
| needs.quality.result == 'success' && | |
| needs.integration_none.result == 'success' && | |
| needs.integration_docker.result == 'success' | |
| ) | |
| ) | |
| }} | |
| environment: | |
| name: 'production-release' | |
| url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.prepare.outputs.release_tag }}' | |
| permissions: | |
| contents: 'write' | |
| packages: 'write' | |
| id-token: 'write' | |
| pull-requests: 'write' | |
| issues: 'write' | |
| steps: | |
| - name: 'Checkout' | |
| uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 | |
| with: | |
| ref: '${{ github.event.inputs.ref || github.sha }}' | |
| fetch-depth: 0 | |
| - name: 'Setup Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'npm' | |
| cache-dependency-path: 'package-lock.json' | |
| registry-url: 'https://registry.npmjs.org' | |
| scope: '@qwen-code' | |
| - name: 'Install Dependencies' | |
| env: | |
| NPM_CONFIG_PREFER_OFFLINE: 'true' | |
| run: |- | |
| npm ci --no-audit --progress=false | |
| - 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' | |
| id: 'release_branch' | |
| env: | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| BRANCH_NAME="release/${RELEASE_TAG}" | |
| git switch -c "${BRANCH_NAME}" | |
| echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}" | |
| - name: 'Update package versions' | |
| env: | |
| RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' | |
| run: |- | |
| npm run release:version "${RELEASE_VERSION}" | |
| - name: 'Commit and Conditionally Push package versions' | |
| env: | |
| BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' | |
| IS_DRY_RUN: '${{ needs.prepare.outputs.is_dry_run }}' | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json | |
| if git diff --staged --quiet; then | |
| echo "No version changes to commit" | |
| else | |
| git commit -m "chore(release): ${RELEASE_TAG}" | |
| fi | |
| if [[ "${IS_DRY_RUN}" == "false" ]]; then | |
| echo "Pushing release branch to remote..." | |
| git push --set-upstream origin "${BRANCH_NAME}" --follow-tags | |
| else | |
| echo "Dry run enabled. Skipping push." | |
| fi | |
| - name: 'Build Bundle and Prepare Package' | |
| run: |- | |
| npm run bundle | |
| npm run prepare:package | |
| - name: 'Build Standalone Archives' | |
| env: | |
| RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' | |
| run: 'npm run package:standalone:release -- --version "${RELEASE_VERSION}" --out-dir dist/standalone' | |
| - name: 'Verify Installation Release Assets' | |
| run: 'npm run verify:installation-release -- --dir dist/standalone' | |
| - name: 'Package Hosted Installation Assets' | |
| env: | |
| RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' | |
| run: 'npm run package:hosted-installation -- --out-dir dist/installation --version "${RELEASE_VERSION}"' | |
| - name: 'Install ossutil' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' }} | |
| env: | |
| OSSUTIL_URL: "${{ vars.OSSUTIL_URL || 'https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip' }}" | |
| OSSUTIL_SHA256: "${{ vars.OSSUTIL_SHA256 || 'dcc512e4a893e16bbee63bc769339d8e56b21744fd83c8212a9d8baf28767343' }}" | |
| run: |- | |
| set -euo pipefail | |
| tmp_dir="$(mktemp -d)" | |
| curl -fsSL --connect-timeout 15 --max-time 300 "${OSSUTIL_URL}" -o "${tmp_dir}/ossutil.zip" | |
| echo "${OSSUTIL_SHA256} ${tmp_dir}/ossutil.zip" | sha256sum -c - | |
| unzip -q "${tmp_dir}/ossutil.zip" -d "${tmp_dir}" | |
| ossutil_path="$(find "${tmp_dir}" -type f \( -name 'ossutil' -o -name 'ossutil64' \) -print -quit)" | |
| if [[ -z "${ossutil_path}" ]]; then | |
| echo "::error::ossutil binary not found in downloaded archive" | |
| exit 1 | |
| fi | |
| chmod +x "${ossutil_path}" | |
| mkdir -p "${HOME}/.local/bin" | |
| install -m 0755 "${ossutil_path}" "${HOME}/.local/bin/ossutil" | |
| echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" | |
| rm -rf "${tmp_dir}" | |
| "${HOME}/.local/bin/ossutil" >/dev/null | |
| - name: 'Configure Aliyun OSS Credentials' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' }} | |
| env: | |
| ALIYUN_OSS_ACCESS_KEY_ID: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}' | |
| ALIYUN_OSS_ACCESS_KEY_SECRET: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}' | |
| ALIYUN_OSS_ENDPOINT: "${{ vars.ALIYUN_OSS_ENDPOINT || 'https://oss-cn-hangzhou.aliyuncs.com' }}" | |
| run: |- | |
| set -euo pipefail | |
| if [[ -z "${ALIYUN_OSS_ACCESS_KEY_ID}" || -z "${ALIYUN_OSS_ACCESS_KEY_SECRET}" ]]; then | |
| echo "::error::Missing Aliyun OSS credentials. Set ALIYUN_OSS_ACCESS_KEY_ID and ALIYUN_OSS_ACCESS_KEY_SECRET in the production-release environment secrets." | |
| exit 1 | |
| fi | |
| ossutil config \ | |
| -e "${ALIYUN_OSS_ENDPOINT}" \ | |
| -i "${ALIYUN_OSS_ACCESS_KEY_ID}" \ | |
| -k "${ALIYUN_OSS_ACCESS_KEY_SECRET}" \ | |
| -L EN \ | |
| -c "${RUNNER_TEMP}/.ossutilconfig" | |
| - name: 'Publish @qwen-code/qwen-code' | |
| working-directory: 'dist' | |
| run: |- | |
| npm publish --access public --tag=${{ needs.prepare.outputs.npm_tag }} ${{ needs.prepare.outputs.is_dry_run == 'true' && '--dry-run' || '' }} | |
| env: | |
| NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' | |
| - name: 'Publish @qwen-code/channel-base' | |
| working-directory: 'packages/channels/base' | |
| run: |- | |
| npm publish --access public --tag=${{ needs.prepare.outputs.npm_tag }} ${{ needs.prepare.outputs.is_dry_run == 'true' && '--dry-run' || '' }} | |
| env: | |
| NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' | |
| - name: 'Create GitHub Release and Tag' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' }} | |
| env: | |
| GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' | |
| RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}' | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| PREVIOUS_RELEASE_TAG: '${{ needs.prepare.outputs.previous_release_tag }}' | |
| IS_NIGHTLY: '${{ needs.prepare.outputs.is_nightly }}' | |
| IS_PREVIEW: '${{ needs.prepare.outputs.is_preview }}' | |
| run: |- | |
| set -euo pipefail | |
| PRERELEASE_FLAG="" | |
| if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then | |
| PRERELEASE_FLAG="--prerelease" | |
| fi | |
| mapfile -t release_assets < <(node scripts/verify-installation-release.js --dir dist/standalone --list-release-asset-paths) | |
| gh release create "${RELEASE_TAG}" \ | |
| dist/cli.js \ | |
| "${release_assets[@]}" \ | |
| --target "${RELEASE_BRANCH}" \ | |
| --title "Release ${RELEASE_TAG}" \ | |
| --notes-start-tag "${PREVIOUS_RELEASE_TAG}" \ | |
| --generate-notes \ | |
| ${PRERELEASE_FLAG} | |
| - name: 'Sync Release Assets to Aliyun OSS' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' }} | |
| env: | |
| ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| mapfile -t release_assets < <(node scripts/verify-installation-release.js --dir dist/standalone --list-release-asset-paths) | |
| node scripts/upload-aliyun-oss-assets.js \ | |
| --bucket "${ALIYUN_OSS_BUCKET}" \ | |
| --config "${RUNNER_TEMP}/.ossutilconfig" \ | |
| --prefix "releases/qwen-code/${RELEASE_TAG}" \ | |
| "${release_assets[@]}" | |
| - name: 'Verify Aliyun OSS Release Assets' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' }} | |
| env: | |
| ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}" | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| npm run verify:installation-release -- --base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/${RELEASE_TAG}" | |
| - name: 'Sync Hosted Installation Assets to Aliyun OSS' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | |
| env: | |
| ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| hosted_assets=( | |
| dist/installation/install-qwen-standalone.sh | |
| dist/installation/install-qwen-standalone.ps1 | |
| dist/installation/install-qwen-standalone.bat | |
| dist/installation/uninstall-qwen-standalone.sh | |
| dist/installation/uninstall-qwen-standalone.ps1 | |
| dist/installation/SHA256SUMS | |
| ) | |
| node scripts/upload-aliyun-oss-assets.js \ | |
| --bucket "${ALIYUN_OSS_BUCKET}" \ | |
| --config "${RUNNER_TEMP}/.ossutilconfig" \ | |
| --prefix "installation/${RELEASE_TAG}" \ | |
| "${hosted_assets[@]}" | |
| node scripts/upload-aliyun-oss-assets.js \ | |
| --bucket "${ALIYUN_OSS_BUCKET}" \ | |
| --config "${RUNNER_TEMP}/.ossutilconfig" \ | |
| --prefix "installation" \ | |
| "${hosted_assets[@]}" | |
| - name: 'Verify Aliyun OSS Hosted Installation Assets' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | |
| env: | |
| ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}" | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| hosted_tmp_dir="$(mktemp -d)" | |
| trap 'rm -rf "${hosted_tmp_dir}"' EXIT | |
| mkdir -p "${hosted_tmp_dir}/versioned" "${hosted_tmp_dir}/global" | |
| for asset in install-qwen-standalone.sh install-qwen-standalone.ps1 install-qwen-standalone.bat uninstall-qwen-standalone.sh uninstall-qwen-standalone.ps1 SHA256SUMS; do | |
| url="${ALIYUN_OSS_PUBLIC_BASE_URL}/installation/${RELEASE_TAG}/${asset}" | |
| global_url="${ALIYUN_OSS_PUBLIC_BASE_URL}/installation/${asset}" | |
| curl -fsSL --connect-timeout 15 --max-time 300 "${url}" -o "${hosted_tmp_dir}/versioned/${asset}" | |
| curl -fsSL --connect-timeout 15 --max-time 300 "${global_url}" -o "${hosted_tmp_dir}/global/${asset}" | |
| done | |
| cmp -s "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/versioned/SHA256SUMS" || { | |
| echo "::error::Hosted installation SHA256SUMS does not match local dist/installation/SHA256SUMS" | |
| diff -u "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/versioned/SHA256SUMS" || true | |
| exit 1 | |
| } | |
| cmp -s "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/global/SHA256SUMS" || { | |
| echo "::error::Global hosted installation SHA256SUMS does not match local dist/installation/SHA256SUMS" | |
| diff -u "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/global/SHA256SUMS" || true | |
| exit 1 | |
| } | |
| (cd "${hosted_tmp_dir}/versioned" && sha256sum -c SHA256SUMS) | |
| (cd "${hosted_tmp_dir}/global" && sha256sum -c SHA256SUMS) | |
| - name: 'Publish Aliyun OSS Latest VERSION' | |
| # Run last so the `latest/VERSION` pointer only flips after every | |
| # release asset and hosted installer object has been uploaded and | |
| # verified. If any earlier step fails, the pointer keeps referring | |
| # to the previously-good release. | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | |
| env: | |
| ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}" | |
| ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}" | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| printf '%s\n' "${RELEASE_TAG}" > "${RUNNER_TEMP}/qwen-code-latest-version" | |
| ossutil cp "${RUNNER_TEMP}/qwen-code-latest-version" "oss://${ALIYUN_OSS_BUCKET}/releases/qwen-code/latest/VERSION" -c "${RUNNER_TEMP}/.ossutilconfig" -f --acl public-read | |
| latest_version="$(curl -fsSL --connect-timeout 15 --max-time 300 "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/latest/VERSION" | tr -d '[:space:]')" | |
| if [[ "${latest_version}" != "${RELEASE_TAG}" ]]; then | |
| echo "::error::Aliyun latest VERSION points to ${latest_version}, expected ${RELEASE_TAG}" | |
| exit 1 | |
| fi | |
| - name: 'Cleanup Aliyun OSS Credentials' | |
| if: |- | |
| ${{ always() && needs.prepare.outputs.is_dry_run == 'false' }} | |
| run: |- | |
| rm -f "${RUNNER_TEMP}/.ossutilconfig" | |
| - name: 'Create PR to merge release branch into main' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | |
| id: 'pr' | |
| env: | |
| GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}' | |
| RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}' | |
| RELEASE_TAG: '${{ needs.prepare.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): ${RELEASE_TAG}" \ | |
| --body "Automated release PR for ${RELEASE_TAG}. Syncs package.json versions on main.")" | |
| fi | |
| echo "PR_URL=${pr_url}" >> "${GITHUB_OUTPUT}" | |
| - name: 'Enable auto-merge for release PR' | |
| if: |- | |
| ${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_nightly == 'false' && needs.prepare.outputs.is_preview == 'false' }} | |
| env: | |
| GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}' | |
| PR_URL: '${{ steps.pr.outputs.PR_URL }}' | |
| RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}' | |
| run: |- | |
| set -euo pipefail | |
| # Keep [skip ci] only on the squash commit that lands on main. The | |
| # release branch commit and PR title intentionally omit it so tag-push | |
| # workflows and PR metadata stay unaffected. | |
| gh pr merge "${PR_URL}" \ | |
| --squash \ | |
| --auto \ | |
| --delete-branch \ | |
| --subject "chore(release): ${RELEASE_TAG} [skip ci]" | |
| notify_failure: | |
| name: 'Notify Release Failure' | |
| runs-on: 'ubuntu-latest' | |
| needs: | |
| - 'prepare' | |
| - 'quality' | |
| - 'integration_none' | |
| - 'integration_docker' | |
| - 'publish' | |
| if: |- | |
| ${{ | |
| always() && | |
| ( | |
| github.event_name == 'schedule' || | |
| github.event.inputs.dry_run != 'true' | |
| ) && | |
| ( | |
| needs.prepare.result == 'failure' || | |
| needs.quality.result == 'failure' || | |
| needs.integration_none.result == 'failure' || | |
| needs.integration_docker.result == 'failure' || | |
| needs.publish.result == 'failure' | |
| ) | |
| }} | |
| permissions: | |
| issues: 'write' | |
| steps: | |
| - name: 'Create Issue on Failure' | |
| env: | |
| GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' | |
| GH_REPO: '${{ github.repository }}' | |
| RELEASE_TAG: "${{ needs.prepare.outputs.release_tag || 'N/A' }}" | |
| DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| run: |- | |
| gh issue create \ | |
| --title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \ | |
| --body "The release workflow failed. See the full run for details: ${DETAILS_URL}" |