diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 782c6afe92..a0b0365ac9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' }} diff --git a/.github/workflows/sync-release-to-oss.yml b/.github/workflows/sync-release-to-oss.yml new file mode 100644 index 0000000000..c2eee4c5fb --- /dev/null +++ b/.github/workflows/sync-release-to-oss.yml @@ -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" diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index 763fe61cc6..c4153dd7a9 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -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)', ); });