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:
易良 2026-05-25 19:34:21 +08:00 committed by GitHub
parent 7cb017d4b0
commit 5493888c15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 309 additions and 234 deletions

View file

@ -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' }}

View 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"

View file

@ -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)',
);
});