mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
288 lines
14 KiB
YAML
288 lines
14 KiB
YAML
name: 'Release Managed server runtime'
|
|
|
|
on:
|
|
release:
|
|
types: ['published']
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Existing published CLI release tag, for example v0.25.4.'
|
|
type: 'string'
|
|
required: true
|
|
dry_run:
|
|
description: 'Build and verify without publishing to Central, GitHub, or OSS.'
|
|
type: 'boolean'
|
|
required: true
|
|
default: true
|
|
|
|
concurrency:
|
|
group: 'managed-runtime-${{ github.event.release.tag_name || inputs.tag }}'
|
|
cancel-in-progress: false
|
|
|
|
permissions:
|
|
contents: 'read'
|
|
|
|
env:
|
|
RELEASE_TAG: '${{ github.event.release.tag_name || inputs.tag }}'
|
|
|
|
jobs:
|
|
prepare:
|
|
if: "${{ github.repository == 'QwenLM/qwen-code' && (github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'v')) }}"
|
|
runs-on: 'ubuntu-latest'
|
|
timeout-minutes: 10
|
|
outputs:
|
|
eligible: '${{ steps.gate.outputs.eligible }}'
|
|
sha: '${{ steps.source.outputs.sha }}'
|
|
version: '${{ steps.source.outputs.version }}'
|
|
timestamp: '${{ steps.source.outputs.timestamp }}'
|
|
steps:
|
|
- name: 'Validate the official published CLI release'
|
|
id: 'gate'
|
|
env:
|
|
GH_TOKEN: '${{ github.token }}'
|
|
EVENT_NAME: '${{ github.event_name }}'
|
|
WORKFLOW_REF: '${{ github.ref }}'
|
|
run: |-
|
|
set -euo pipefail
|
|
if [[ "${EVENT_NAME}" == 'workflow_dispatch' && "${WORKFLOW_REF}" != 'refs/heads/main' ]]; then
|
|
echo 'Manual Managed runtime releases must run from main.' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*)?$ ]]; then
|
|
if [[ "${EVENT_NAME}" == 'release' ]]; then
|
|
echo 'eligible=false' >> "${GITHUB_OUTPUT}"
|
|
exit 0
|
|
fi
|
|
echo 'An exact CLI release tag is required.' >&2
|
|
exit 1
|
|
fi
|
|
gh api "repos/QwenLM/qwen-code/releases/tags/${RELEASE_TAG}" > "${RUNNER_TEMP}/managed-release.json"
|
|
jq --exit-status --arg tag "${RELEASE_TAG}" '.draft == false and .tag_name == $tag' "${RUNNER_TEMP}/managed-release.json"
|
|
echo 'eligible=true' >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: 'Checkout the existing release tag'
|
|
if: "${{ steps.gate.outputs.eligible == 'true' }}"
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
with:
|
|
ref: '${{ env.RELEASE_TAG }}'
|
|
persist-credentials: false
|
|
|
|
- name: 'Pin the source and exact CLI version'
|
|
id: 'source'
|
|
if: "${{ steps.gate.outputs.eligible == 'true' }}"
|
|
run: |-
|
|
set -euo pipefail
|
|
version=$(node -p 'JSON.parse(require("fs").readFileSync("package.json", "utf8")).version')
|
|
if [[ "${RELEASE_TAG}" != "v${version}" ]]; then
|
|
echo 'Release tag and CLI package version differ.' >&2
|
|
exit 1
|
|
fi
|
|
test -f scripts/managed-runtime-release.js
|
|
printf 'sha=%s\nversion=%s\ntimestamp=%s\n' "$(git rev-parse HEAD)" "${version}" "$(git show -s --format=%cI HEAD)" >> "${GITHUB_OUTPUT}"
|
|
|
|
release:
|
|
needs: ['prepare']
|
|
if: "${{ needs.prepare.outputs.eligible == 'true' }}"
|
|
runs-on: 'ubuntu-latest'
|
|
timeout-minutes: 90
|
|
environment:
|
|
name: 'production-release'
|
|
permissions:
|
|
contents: 'write'
|
|
env:
|
|
CLI_VERSION: '${{ needs.prepare.outputs.version }}'
|
|
SOURCE_SHA: '${{ needs.prepare.outputs.sha }}'
|
|
BUILD_TIMESTAMP: '${{ needs.prepare.outputs.timestamp }}'
|
|
ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}"
|
|
OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
|
|
steps:
|
|
- name: 'Checkout the pinned source'
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
with:
|
|
ref: '${{ needs.prepare.outputs.sha }}'
|
|
persist-credentials: false
|
|
|
|
- name: 'Checkout release tooling from the workflow revision'
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
with:
|
|
ref: '${{ github.workflow_sha }}'
|
|
path: '.qwen/managed-release-tools'
|
|
sparse-checkout: 'scripts'
|
|
persist-credentials: false
|
|
|
|
- name: 'Set up Java 21'
|
|
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
|
with:
|
|
distribution: 'temurin'
|
|
java-version: '21'
|
|
cache: 'maven'
|
|
cache-dependency-path: |
|
|
packages/sdk-java/qwencode/pom.xml
|
|
packages/managed-agent-server/pom.xml
|
|
|
|
- name: 'Set up Node.js 22'
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
package-manager-cache: false
|
|
|
|
- name: 'Build the matching CLI prerequisites'
|
|
env:
|
|
QWEN_SKIP_PREPARE: '1'
|
|
run: |-
|
|
corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only
|
|
npm run build
|
|
npm run typecheck
|
|
npm run bundle
|
|
|
|
- name: 'Verify and install the SDK from the same source'
|
|
run: |-
|
|
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml clean install -Dgpg.skip=true -Dproject.build.outputTimestamp="${BUILD_TIMESTAMP}"
|
|
|
|
- name: 'Set the server artifact to the exact CLI version'
|
|
run: |-
|
|
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml versions:set -DnewVersion="${CLI_VERSION}" -DgenerateBackupPoms=false
|
|
|
|
- name: 'Verify the executable JAR and publication profile'
|
|
run: |-
|
|
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml clean verify -Pcentral-release -Dgpg.skip=true -Dproject.build.outputTimestamp="${BUILD_TIMESTAMP}"
|
|
node .qwen/managed-release-tools/scripts/managed-runtime-release.js --jar "packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}.jar" --version "${CLI_VERSION}" --out-dir dist/managed-runtime
|
|
|
|
- name: 'Retain verified JAR and checksum'
|
|
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
|
with:
|
|
name: 'managed-runtime-${{ needs.prepare.outputs.version }}'
|
|
path: 'dist/managed-runtime/*'
|
|
if-no-files-found: 'error'
|
|
|
|
- name: 'Install the verified OSS uploader'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
env:
|
|
OSSUTIL_URL: 'https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip'
|
|
OSSUTIL_SHA256: 'dcc512e4a893e16bbee63bc769339d8e56b21744fd83c8212a9d8baf28767343'
|
|
run: |-
|
|
set -euo pipefail
|
|
work=$(mktemp -d)
|
|
trap 'rm -rf "${work}"' EXIT
|
|
curl -fsSL --connect-timeout 15 --max-time 300 "${OSSUTIL_URL}" -o "${work}/ossutil.zip"
|
|
echo "${OSSUTIL_SHA256} ${work}/ossutil.zip" | sha256sum -c -
|
|
unzip -q "${work}/ossutil.zip" -d "${work}"
|
|
binary=$(find "${work}" -type f -name 'ossutil64' -print -quit)
|
|
test -n "${binary}"
|
|
mkdir -p "${HOME}/.local/bin"
|
|
install -m 0755 "${binary}" "${HOME}/.local/bin/ossutil"
|
|
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
|
|
|
|
- name: 'Configure authenticated OSS access'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
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
|
|
umask 077
|
|
[[ -n "${ALIYUN_OSS_ACCESS_KEY_ID}" && -n "${ALIYUN_OSS_ACCESS_KEY_SECRET}" ]]
|
|
ossutil config -e "${ALIYUN_OSS_ENDPOINT}" -i "${ALIYUN_OSS_ACCESS_KEY_ID}" -k "${ALIYUN_OSS_ACCESS_KEY_SECRET}" -L EN -c "${RUNNER_TEMP}/managed-ossutilconfig"
|
|
|
|
- name: 'Recheck the immutable tag and existing publication bytes'
|
|
id: 'publication'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
env:
|
|
GH_TOKEN: '${{ github.token }}'
|
|
run: |-
|
|
set -euo pipefail
|
|
direct=$(git ls-remote --tags origin "refs/tags/${RELEASE_TAG}" | cut -f1)
|
|
peeled=$(git ls-remote --tags origin "refs/tags/${RELEASE_TAG}^{}" | cut -f1)
|
|
if [[ "${peeled:-${direct}}" != "${SOURCE_SHA}" ]]; then
|
|
echo 'The release tag moved after verification; refusing publication.' >&2
|
|
exit 1
|
|
fi
|
|
gh api "repos/QwenLM/qwen-code/releases/tags/${RELEASE_TAG}" | jq --exit-status '.draft == false'
|
|
jar="qwen-managed-agent-server-${CLI_VERSION}.jar"
|
|
central="https://repo.maven.apache.org/maven2/com/alibaba/qwen-managed-agent-server/${CLI_VERSION}/${jar}"
|
|
state=$(node .qwen/managed-release-tools/scripts/managed-runtime-release.js --check-url "${central}" --file "dist/managed-runtime/${jar}") || exit 1
|
|
echo "central=${state}" >> "${GITHUB_OUTPUT}"
|
|
: > "${RUNNER_TEMP}/managed-github-missing"
|
|
: > "${RUNNER_TEMP}/managed-oss-missing"
|
|
for asset in "${jar}" "${jar}.sha256"; do
|
|
github="https://github.com/QwenLM/qwen-code/releases/download/${RELEASE_TAG}/${asset}"
|
|
oss="oss://${ALIYUN_OSS_BUCKET}/releases/qwen-code/${RELEASE_TAG}/${asset}"
|
|
state=$(node .qwen/managed-release-tools/scripts/managed-runtime-release.js --check-url "${github}" --file "dist/managed-runtime/${asset}") || exit 1
|
|
if [[ "${state}" == 'missing' ]]; then
|
|
echo "dist/managed-runtime/${asset}" >> "${RUNNER_TEMP}/managed-github-missing"
|
|
fi
|
|
state=$(node .qwen/managed-release-tools/scripts/managed-runtime-release.js --check-oss "${oss}" --config "${RUNNER_TEMP}/managed-ossutilconfig" --file "dist/managed-runtime/${asset}") || exit 1
|
|
if [[ "${state}" == 'missing' ]]; then
|
|
echo "dist/managed-runtime/${asset}" >> "${RUNNER_TEMP}/managed-oss-missing"
|
|
fi
|
|
done
|
|
|
|
- name: 'Configure signed Central publication'
|
|
if: "${{ (github.event_name == 'release' || !inputs.dry_run) && steps.publication.outputs.central == 'missing' }}"
|
|
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
|
with:
|
|
distribution: 'temurin'
|
|
java-version: '21'
|
|
server-id: 'central'
|
|
server-username: 'MAVEN_USERNAME'
|
|
server-password: 'MAVEN_PASSWORD'
|
|
gpg-private-key: '${{ secrets.MAVEN_GPG_PRIVATE_KEY }}'
|
|
gpg-passphrase: 'MAVEN_GPG_PASSPHRASE'
|
|
|
|
- name: 'Publish the signed server artifact to Central'
|
|
if: "${{ (github.event_name == 'release' || !inputs.dry_run) && steps.publication.outputs.central == 'missing' }}"
|
|
env:
|
|
MAVEN_USERNAME: '${{ secrets.CENTRAL_USERNAME }}'
|
|
MAVEN_PASSWORD: '${{ secrets.CENTRAL_PASSWORD }}'
|
|
MAVEN_GPG_PRIVATE_KEY: '${{ secrets.MAVEN_GPG_PRIVATE_KEY }}'
|
|
MAVEN_GPG_PASSPHRASE: '${{ secrets.MAVEN_GPG_PASSPHRASE }}'
|
|
run: |-
|
|
set -euo pipefail
|
|
for name in MAVEN_USERNAME MAVEN_PASSWORD MAVEN_GPG_PRIVATE_KEY MAVEN_GPG_PASSPHRASE; do
|
|
[[ -n "${!name}" ]] || { echo "Missing production-release secret: ${name}" >&2; exit 1; }
|
|
done
|
|
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml deploy -Pcentral-release -DskipTests -Dproject.build.outputTimestamp="${BUILD_TIMESTAMP}"
|
|
cmp "packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}.jar" "dist/managed-runtime/qwen-managed-agent-server-${CLI_VERSION}.jar"
|
|
|
|
- name: 'Verify Central availability before distribution'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
run: |-
|
|
set -euo pipefail
|
|
jar="qwen-managed-agent-server-${CLI_VERSION}.jar"
|
|
for attempt in {1..40}; do
|
|
state=$(node .qwen/managed-release-tools/scripts/managed-runtime-release.js --check-url "https://repo.maven.apache.org/maven2/com/alibaba/qwen-managed-agent-server/${CLI_VERSION}/${jar}" --file "dist/managed-runtime/${jar}")
|
|
if [[ "${state}" == 'present' ]]; then exit 0; fi
|
|
sleep 30
|
|
done
|
|
echo 'The exact server JAR is not yet available on Central.' >&2
|
|
exit 1
|
|
|
|
- name: 'Attach only missing GitHub release assets'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
env:
|
|
GH_TOKEN: '${{ github.token }}'
|
|
run: |-
|
|
set -euo pipefail
|
|
mapfile -t assets < "${RUNNER_TEMP}/managed-github-missing"
|
|
if (( ${#assets[@]} )); then gh release upload "${RELEASE_TAG}" "${assets[@]}"; fi
|
|
|
|
- name: 'Upload only missing OSS release assets'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
run: |-
|
|
set -euo pipefail
|
|
mapfile -t assets < "${RUNNER_TEMP}/managed-oss-missing"
|
|
if (( ${#assets[@]} == 0 )); then exit 0; fi
|
|
node scripts/upload-aliyun-oss-assets.js --bucket "${ALIYUN_OSS_BUCKET}" --config "${RUNNER_TEMP}/managed-ossutilconfig" --prefix "releases/qwen-code/${RELEASE_TAG}" "${assets[@]}"
|
|
|
|
- name: 'Verify both public download channels'
|
|
if: "${{ github.event_name == 'release' || !inputs.dry_run }}"
|
|
run: |-
|
|
set -euo pipefail
|
|
jar="qwen-managed-agent-server-${CLI_VERSION}.jar"
|
|
for base in "https://github.com/QwenLM/qwen-code/releases/download/${RELEASE_TAG}" "${OSS_PUBLIC_BASE_URL}/releases/qwen-code/${RELEASE_TAG}"; do
|
|
for asset in "${jar}" "${jar}.sha256"; do
|
|
[[ "$(node .qwen/managed-release-tools/scripts/managed-runtime-release.js --check-url "${base}/${asset}" --file "dist/managed-runtime/${asset}")" == 'present' ]]
|
|
done
|
|
done
|