Files
qwen-code/.github/workflows/release-managed-runtime.yml
T

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