mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
ci: split Aliyun OSS sync into a separate post-release workflow (#4492)
* ci: split Aliyun OSS sync into a separate post-release workflow The OSS upload and verification steps were adding significant time to the release workflow's critical path. Move them into a new `sync-release-to-oss.yml` workflow that triggers on `release: published`, running asynchronously after the release completes. Key changes: - Extract all OSS steps (ossutil install, credential config, asset upload, verification, hosted installation sync, latest VERSION pointer) into `sync-release-to-oss.yml` - Switch `gh release create` to use CI_BOT_PAT so the release event can trigger the new downstream workflow (GITHUB_TOKEN events don't trigger other workflows) - Add `workflow_dispatch` input for manual re-runs on failure - New workflow downloads release assets from GitHub Release instead of rebuilding them This decouples publishing from CDN distribution: the release finishes as soon as npm publish + GitHub Release are done, and China CDN sync happens in parallel without blocking. * fix(test): update install-script test to check sync-release-to-oss.yml The test asserts OSS sync steps exist in the workflow. Now that these steps live in sync-release-to-oss.yml instead of release.yml, update the test to read from the correct file and add assertions that release.yml no longer contains OSS logic. * fix(ci): address review feedback for OSS sync split - Add 'Verify Standalone Archives' step before gh release create in release.yml as a pre-publish safety gate (wenshao) - Add concurrency group to sync-release-to-oss.yml to prevent race conditions when multiple releases publish close together (wenshao) - Update test to assert verify step exists in release.yml * chore: add comment explaining CI_BOT_PAT requirement [skip ci]
This commit is contained in:
parent
7cb017d4b0
commit
5493888c15
3 changed files with 309 additions and 234 deletions
186
.github/workflows/release.yml
vendored
186
.github/workflows/release.yml
vendored
|
|
@ -386,63 +386,6 @@ jobs:
|
|||
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: |-
|
||||
|
|
@ -457,150 +400,37 @@ jobs:
|
|||
env:
|
||||
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
|
||||
|
||||
- name: 'Verify Standalone Archives'
|
||||
run: |-
|
||||
npm run verify:installation-release -- --dir dist/standalone
|
||||
|
||||
- name: 'Create GitHub Release and Tag'
|
||||
if: |-
|
||||
${{ needs.prepare.outputs.is_dry_run == 'false' }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
# CI_BOT_PAT required: GITHUB_TOKEN events cannot trigger downstream workflows (sync-release-to-oss.yml).
|
||||
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
|
||||
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[@]}" \
|
||||
dist/standalone/qwen-code-* \
|
||||
dist/standalone/SHA256SUMS \
|
||||
--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' }}
|
||||
|
|
|
|||
238
.github/workflows/sync-release-to-oss.yml
vendored
Normal file
238
.github/workflows/sync-release-to-oss.yml
vendored
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
name: 'Sync Release to Aliyun OSS'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: ['published']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'The release tag to sync (e.g., v0.1.11).'
|
||||
required: true
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: 'sync-release-to-oss'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: 'Sync Release Assets to Aliyun OSS'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: |-
|
||||
${{ github.repository == 'QwenLM/qwen-code' }}
|
||||
environment:
|
||||
name: 'production-release'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
env:
|
||||
RELEASE_TAG: '${{ github.event.release.tag_name || inputs.tag }}'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
|
||||
with:
|
||||
ref: '${{ env.RELEASE_TAG }}'
|
||||
|
||||
- name: 'Determine release type'
|
||||
id: 'meta'
|
||||
env:
|
||||
TAG: '${{ env.RELEASE_TAG }}'
|
||||
run: |-
|
||||
is_nightly="false"
|
||||
is_preview="false"
|
||||
if [[ "${TAG}" == *"nightly"* ]]; then
|
||||
is_nightly="true"
|
||||
elif [[ "${TAG}" == *"preview"* ]]; then
|
||||
is_preview="true"
|
||||
fi
|
||||
echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}"
|
||||
echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}"
|
||||
echo "is_stable=$([[ ${is_nightly} == 'false' && ${is_preview} == 'false' ]] && echo true || echo false)" >> "${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: 'Download Release Assets from GitHub'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p dist/standalone
|
||||
gh release download "${RELEASE_TAG}" --dir dist/standalone --pattern '*.tar.gz' --pattern '*.zip' --pattern 'SHA256SUMS'
|
||||
|
||||
- name: 'Verify Downloaded Release Assets'
|
||||
run: |-
|
||||
npm run verify:installation-release -- --dir dist/standalone
|
||||
|
||||
- name: 'Package Hosted Installation Assets'
|
||||
if: |-
|
||||
${{ steps.meta.outputs.is_stable == 'true' }}
|
||||
env:
|
||||
RELEASE_TAG: '${{ env.RELEASE_TAG }}'
|
||||
run: |-
|
||||
RELEASE_VERSION="${RELEASE_TAG#v}"
|
||||
npm run package:hosted-installation -- --out-dir dist/installation --version "${RELEASE_VERSION}"
|
||||
|
||||
- name: 'Install ossutil'
|
||||
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'
|
||||
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: 'Sync Release Assets to Aliyun OSS'
|
||||
env:
|
||||
ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}"
|
||||
RELEASE_TAG: '${{ env.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'
|
||||
env:
|
||||
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
|
||||
RELEASE_TAG: '${{ env.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: |-
|
||||
${{ steps.meta.outputs.is_stable == 'true' }}
|
||||
env:
|
||||
ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}"
|
||||
RELEASE_TAG: '${{ env.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: |-
|
||||
${{ steps.meta.outputs.is_stable == 'true' }}
|
||||
env:
|
||||
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
|
||||
RELEASE_TAG: '${{ env.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'
|
||||
if: |-
|
||||
${{ steps.meta.outputs.is_stable == 'true' }}
|
||||
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: '${{ env.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() }}'
|
||||
run: |-
|
||||
rm -f "${RUNNER_TEMP}/.ossutilconfig"
|
||||
|
|
@ -1646,63 +1646,70 @@ describe('standalone release packaging', () => {
|
|||
});
|
||||
|
||||
it('syncs standalone and hosted installation assets during release', () => {
|
||||
const workflow = readScript('.github/workflows/release.yml');
|
||||
const releaseWorkflow = readScript('.github/workflows/release.yml');
|
||||
const ossWorkflow = readScript('.github/workflows/sync-release-to-oss.yml');
|
||||
|
||||
expect(workflow).toContain('npm run package:standalone:release --');
|
||||
expect(workflow).toContain(
|
||||
'npm run package:hosted-installation -- --out-dir dist/installation',
|
||||
);
|
||||
expect(workflow).not.toContain('package:installation-assets');
|
||||
expect(workflow).not.toContain('verify_node_checksum()');
|
||||
expect(workflow).not.toContain('download_node()');
|
||||
expect(workflow).not.toContain('dist/standalone/qwen-code-*.tar.gz');
|
||||
expect(workflow).not.toContain('dist/standalone/qwen-code-*.zip');
|
||||
expect(workflow).toContain('--list-release-asset-paths');
|
||||
expect(workflow).toContain(
|
||||
// release.yml builds standalone archives, verifies them, and creates GitHub Release
|
||||
expect(releaseWorkflow).toContain('npm run package:standalone:release --');
|
||||
expect(releaseWorkflow).toContain(
|
||||
'npm run verify:installation-release -- --dir dist/standalone',
|
||||
);
|
||||
expect(workflow).toContain('secrets.ALIYUN_OSS_ACCESS_KEY_ID');
|
||||
expect(workflow).toContain('secrets.ALIYUN_OSS_ACCESS_KEY_SECRET');
|
||||
expect(workflow).toContain('vars.ALIYUN_OSS_BUCKET');
|
||||
expect(workflow).toContain('vars.ALIYUN_OSS_ENDPOINT');
|
||||
expect(workflow).toContain('vars.OSSUTIL_URL');
|
||||
expect(workflow).toContain('vars.OSSUTIL_SHA256');
|
||||
expect(workflow).not.toContain('sudo install');
|
||||
expect(workflow).toContain('${HOME}/.local/bin/ossutil');
|
||||
expect(workflow).toContain('${GITHUB_PATH}');
|
||||
expect(existsSync('scripts/upload-aliyun-oss-assets.js')).toBe(true);
|
||||
expect(workflow).toContain('node scripts/upload-aliyun-oss-assets.js');
|
||||
expect(workflow.match(/upload_asset\(\)/g) || []).toHaveLength(0);
|
||||
expect(workflow).toContain('releases/qwen-code/${RELEASE_TAG}');
|
||||
expect(workflow).toContain('releases/qwen-code/latest');
|
||||
expect(workflow).not.toContain(
|
||||
'upload_release_assets "releases/qwen-code/latest"',
|
||||
);
|
||||
const createReleaseStepIndex = workflow.indexOf(
|
||||
expect(releaseWorkflow).not.toContain('package:installation-assets');
|
||||
expect(releaseWorkflow).not.toContain('verify_node_checksum()');
|
||||
expect(releaseWorkflow).not.toContain('download_node()');
|
||||
const createReleaseStepIndex = releaseWorkflow.indexOf(
|
||||
"name: 'Create GitHub Release and Tag'",
|
||||
);
|
||||
expect(createReleaseStepIndex).toBeGreaterThanOrEqual(0);
|
||||
const createReleaseStep = workflow.slice(createReleaseStepIndex);
|
||||
expect(createReleaseStep).toContain('mapfile -t release_assets');
|
||||
expect(createReleaseStep).toContain('"${release_assets[@]}"');
|
||||
expect(createReleaseStep).not.toContain(
|
||||
'dist/standalone/qwen-code-*.tar.gz',
|
||||
const createReleaseStep = releaseWorkflow.slice(createReleaseStepIndex);
|
||||
expect(createReleaseStep).toContain('dist/standalone/qwen-code-*');
|
||||
expect(createReleaseStep).toContain('dist/standalone/SHA256SUMS');
|
||||
// OSS upload logic must not remain in release.yml
|
||||
expect(releaseWorkflow).not.toContain('secrets.ALIYUN_OSS_ACCESS_KEY_ID');
|
||||
expect(releaseWorkflow).not.toContain(
|
||||
'node scripts/upload-aliyun-oss-assets.js',
|
||||
);
|
||||
expect(createReleaseStep).not.toContain('dist/standalone/qwen-code-*.zip');
|
||||
expect(releaseWorkflow).not.toContain('package:hosted-installation');
|
||||
|
||||
const syncStepIndex = workflow.indexOf(
|
||||
// sync-release-to-oss.yml handles OSS sync triggered by release publish
|
||||
expect(ossWorkflow).toContain(
|
||||
'npm run package:hosted-installation -- --out-dir dist/installation',
|
||||
);
|
||||
expect(ossWorkflow).toContain('--list-release-asset-paths');
|
||||
expect(ossWorkflow).toContain(
|
||||
'npm run verify:installation-release -- --dir dist/standalone',
|
||||
);
|
||||
expect(ossWorkflow).toContain('secrets.ALIYUN_OSS_ACCESS_KEY_ID');
|
||||
expect(ossWorkflow).toContain('secrets.ALIYUN_OSS_ACCESS_KEY_SECRET');
|
||||
expect(ossWorkflow).toContain('vars.ALIYUN_OSS_BUCKET');
|
||||
expect(ossWorkflow).toContain('vars.ALIYUN_OSS_ENDPOINT');
|
||||
expect(ossWorkflow).toContain('vars.OSSUTIL_URL');
|
||||
expect(ossWorkflow).toContain('vars.OSSUTIL_SHA256');
|
||||
expect(ossWorkflow).not.toContain('sudo install');
|
||||
expect(ossWorkflow).toContain('${HOME}/.local/bin/ossutil');
|
||||
expect(ossWorkflow).toContain('${GITHUB_PATH}');
|
||||
expect(existsSync('scripts/upload-aliyun-oss-assets.js')).toBe(true);
|
||||
expect(ossWorkflow).toContain('node scripts/upload-aliyun-oss-assets.js');
|
||||
expect(ossWorkflow.match(/upload_asset\(\)/g) || []).toHaveLength(0);
|
||||
expect(ossWorkflow).toContain('releases/qwen-code/${RELEASE_TAG}');
|
||||
expect(ossWorkflow).toContain('releases/qwen-code/latest');
|
||||
expect(ossWorkflow).not.toContain(
|
||||
'upload_release_assets "releases/qwen-code/latest"',
|
||||
);
|
||||
|
||||
const syncStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Sync Release Assets to Aliyun OSS'",
|
||||
);
|
||||
const verifyStepIndex = workflow.indexOf(
|
||||
const verifyStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Verify Aliyun OSS Release Assets'",
|
||||
);
|
||||
const publishLatestStepIndex = workflow.indexOf(
|
||||
const publishLatestStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Publish Aliyun OSS Latest VERSION'",
|
||||
);
|
||||
const syncHostedStepIndex = workflow.indexOf(
|
||||
const syncHostedStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Sync Hosted Installation Assets to Aliyun OSS'",
|
||||
);
|
||||
const verifyHostedStepIndex = workflow.indexOf(
|
||||
const verifyHostedStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Verify Aliyun OSS Hosted Installation Assets'",
|
||||
);
|
||||
expect(syncStepIndex).toBeGreaterThanOrEqual(0);
|
||||
|
|
@ -1712,16 +1719,16 @@ describe('standalone release packaging', () => {
|
|||
// Latest VERSION pointer must flip only after every release asset and
|
||||
// hosted installer object is uploaded and verified.
|
||||
expect(publishLatestStepIndex).toBeGreaterThan(verifyHostedStepIndex);
|
||||
expect(workflow.slice(syncStepIndex, verifyStepIndex)).not.toContain(
|
||||
expect(ossWorkflow.slice(syncStepIndex, verifyStepIndex)).not.toContain(
|
||||
'releases/qwen-code/latest/VERSION',
|
||||
);
|
||||
expect(workflow.slice(publishLatestStepIndex)).toContain(
|
||||
expect(ossWorkflow.slice(publishLatestStepIndex)).toContain(
|
||||
'releases/qwen-code/latest/VERSION',
|
||||
);
|
||||
const syncStep = workflow.slice(syncStepIndex, verifyStepIndex);
|
||||
const syncStep = ossWorkflow.slice(syncStepIndex, verifyStepIndex);
|
||||
expect(syncStep).not.toContain('dist/installation/');
|
||||
expect(syncStep).not.toContain('installation/install-qwen-standalone.sh');
|
||||
const syncHostedStep = workflow.slice(
|
||||
const syncHostedStep = ossWorkflow.slice(
|
||||
syncHostedStepIndex,
|
||||
verifyHostedStepIndex,
|
||||
);
|
||||
|
|
@ -1748,22 +1755,22 @@ describe('standalone release packaging', () => {
|
|||
const uploadScript = readScript('scripts/upload-aliyun-oss-assets.js');
|
||||
expect(uploadScript).toContain("'--acl'");
|
||||
expect(uploadScript).toContain("'public-read'");
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'curl -fsSL --connect-timeout 15 --max-time 300 "${OSSUTIL_URL}"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'npm run verify:installation-release -- --base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/${RELEASE_TAG}"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'latest_version="$(curl -fsSL --connect-timeout 15 --max-time 300 "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/latest/VERSION" | tr -d',
|
||||
);
|
||||
expect(workflow).not.toContain(
|
||||
expect(ossWorkflow).not.toContain(
|
||||
'npm run verify:installation-release -- --base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/latest"',
|
||||
);
|
||||
const verifyStep = workflow.slice(verifyStepIndex, syncHostedStepIndex);
|
||||
const verifyStep = ossWorkflow.slice(verifyStepIndex, syncHostedStepIndex);
|
||||
expect(verifyStep).not.toContain('hosted_tmp_dir');
|
||||
const verifyHostedStep = workflow.slice(verifyHostedStepIndex);
|
||||
expect(workflow).toContain('hosted_tmp_dir="$(mktemp -d)"');
|
||||
const verifyHostedStep = ossWorkflow.slice(verifyHostedStepIndex);
|
||||
expect(ossWorkflow).toContain('hosted_tmp_dir="$(mktemp -d)"');
|
||||
expect(verifyHostedStep).toContain(
|
||||
'url="${ALIYUN_OSS_PUBLIC_BASE_URL}/installation/${RELEASE_TAG}/${asset}"',
|
||||
);
|
||||
|
|
@ -1776,16 +1783,16 @@ describe('standalone release packaging', () => {
|
|||
expect(verifyHostedStep).toContain(
|
||||
'curl -fsSL --connect-timeout 15 --max-time 300 "${global_url}"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'cmp -s "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/versioned/SHA256SUMS"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'cmp -s "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/global/SHA256SUMS"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'(cd "${hosted_tmp_dir}/versioned" && sha256sum -c SHA256SUMS)',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
expect(ossWorkflow).toContain(
|
||||
'(cd "${hosted_tmp_dir}/global" && sha256sum -c SHA256SUMS)',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue