diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 063116dd6..5e1c17ded 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -13,8 +13,22 @@ on: description: "Release title. Defaults to the tag." required: false type: string + qwen_code_source: + description: "Qwen Code runtime source to vendor into the desktop app." + required: true + default: pinned_package_version + type: choice + options: + - npm_latest + - source_branch + - pinned_package_version + qwen_code_ref: + description: "QwenLM/qwen-code branch, tag, or commit when qwen_code_source is source_branch." + required: false + default: main + type: string qwen_code_version: - description: "Optional @qwen-code/qwen-code version to vendor into the desktop app." + description: "Optional exact @qwen-code/qwen-code npm version for pinned_package_version. Defaults to package.json qwenCodeRuntime.version." required: false type: string dry_run: @@ -48,7 +62,6 @@ concurrency: env: BUN_VERSION: 1.3.9 CRAFT_BRAND: openwork - QWEN_CODE_VERSION: ${{ inputs.qwen_code_version }} jobs: release_metadata: @@ -166,6 +179,14 @@ jobs: with: ref: ${{ needs.release_metadata.outputs.release_ref }} + - name: Check out Qwen Code source + if: ${{ inputs.qwen_code_source == 'source_branch' }} + uses: actions/checkout@v4 + with: + repository: QwenLM/qwen-code + ref: ${{ inputs.qwen_code_ref }} + path: qwen-code-source + - name: Set up Bun uses: oven-sh/setup-bun@v2 with: @@ -180,12 +201,54 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Install Qwen Code source dependencies + if: ${{ inputs.qwen_code_source == 'source_branch' }} + working-directory: qwen-code-source + run: npm ci + - name: Bump desktop version run: bun run bump-desktop-version "${{ needs.release_metadata.outputs.version }}" - name: Confirm release version run: bun run check-release-version --version "${{ needs.release_metadata.outputs.version }}" + - name: Configure Qwen Code runtime source + shell: bash + env: + QWEN_CODE_REF_INPUT: ${{ inputs.qwen_code_ref }} + QWEN_CODE_SOURCE_INPUT: ${{ inputs.qwen_code_source }} + QWEN_CODE_SOURCE_ROOT: ${{ github.workspace }}/qwen-code-source + QWEN_CODE_VERSION_INPUT: ${{ inputs.qwen_code_version }} + run: | + set -euo pipefail + + case "$QWEN_CODE_SOURCE_INPUT" in + npm_latest) + echo "QWEN_CODE_VERSION=latest" >> "$GITHUB_ENV" + echo "Using Qwen Code runtime from npm dist-tag: latest" + ;; + source_branch) + if [ -z "$QWEN_CODE_REF_INPUT" ]; then + echo "::error::qwen_code_ref is required when qwen_code_source is source_branch." + exit 1 + fi + echo "QWEN_CODE_ROOT=$QWEN_CODE_SOURCE_ROOT" >> "$GITHUB_ENV" + echo "Using Qwen Code runtime from QwenLM/qwen-code ref: $QWEN_CODE_REF_INPUT" + ;; + pinned_package_version) + if [ -n "$QWEN_CODE_VERSION_INPUT" ]; then + echo "QWEN_CODE_VERSION=$QWEN_CODE_VERSION_INPUT" >> "$GITHUB_ENV" + echo "Using exact Qwen Code npm version: $QWEN_CODE_VERSION_INPUT" + else + echo "Using Qwen Code runtime from package.json qwenCodeRuntime.version" + fi + ;; + *) + echo "::error::Unknown qwen_code_source: $QWEN_CODE_SOURCE_INPUT" + exit 1 + ;; + esac + - name: Configure optional signing secrets shell: bash env: diff --git a/scripts/vendor-qwen-code.ts b/scripts/vendor-qwen-code.ts index d77a1a418..020739290 100644 --- a/scripts/vendor-qwen-code.ts +++ b/scripts/vendor-qwen-code.ts @@ -14,6 +14,8 @@ const desktopRoot = join(import.meta.dir, '..'); const defaultRepoRoot = join(desktopRoot, '..', '..'); const electronDir = join(desktopRoot, 'apps', 'electron'); const vendorDir = join(electronDir, 'vendor', 'qwen-code'); +const qwenCodePackageName = '@qwen-code/qwen-code'; +const qwenCodeMetadataUrl = `https://registry.npmjs.org/${encodeURIComponent(qwenCodePackageName)}`; interface DesktopPackageJson { qwenCodeRuntime?: { @@ -21,6 +23,18 @@ interface DesktopPackageJson { }; } +interface NpmPackageMetadata { + 'dist-tags'?: Record; + versions?: Record< + string, + { + dist?: { + tarball?: string; + }; + } + >; +} + function npmCommand(): string { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; } @@ -106,17 +120,59 @@ async function vendorLocalCheckout(repoRoot: string): Promise { console.log(`Vendored local Qwen Code CLI into ${vendorDir}`); } -async function vendorNpmVersion(version: string): Promise { - console.log(`Downloading Qwen Code ${version} from npm...`); +async function readNpmPackageMetadata(): Promise { + const response = await fetch(qwenCodeMetadataUrl); + if (!response.ok) { + throw new Error( + `Failed to read ${qwenCodePackageName} metadata from npm: HTTP ${response.status}`, + ); + } + return (await response.json()) as NpmPackageMetadata; +} + +async function resolveNpmVersionOrTag( + versionOrTag: string, +): Promise<{ tarballUrl: string; version: string }> { + const requested = versionOrTag.trim(); + if (!requested) { + throw new Error('Qwen Code npm version or dist-tag is required.'); + } + + const metadata = await readNpmPackageMetadata(); + const version = metadata.versions?.[requested] + ? requested + : metadata['dist-tags']?.[requested]; + if (!version) { + throw new Error( + `Could not resolve ${qwenCodePackageName}@${requested} from npm.`, + ); + } + + const tarballUrl = metadata.versions?.[version]?.dist?.tarball; + if (!tarballUrl) { + throw new Error( + `Could not find npm tarball for ${qwenCodePackageName}@${version}.`, + ); + } + + return { tarballUrl, version }; +} + +async function vendorNpmVersion(versionOrTag: string): Promise { + const { tarballUrl, version } = await resolveNpmVersionOrTag(versionOrTag); + const sourceLabel = + versionOrTag === version ? version : `${versionOrTag} (${version})`; + console.log(`Downloading Qwen Code ${sourceLabel} from npm...`); const tempDir = mkdtempSync(join(tmpdir(), 'qwen-code-vendor-')); const tarballPath = join(tempDir, `qwen-code-${version}.tgz`); - const url = `https://registry.npmjs.org/@qwen-code/qwen-code/-/qwen-code-${version}.tgz`; try { - const response = await fetch(url); + const response = await fetch(tarballUrl); if (!response.ok) { - throw new Error(`Failed to download ${url}: HTTP ${response.status}`); + throw new Error( + `Failed to download ${tarballUrl}: HTTP ${response.status}`, + ); } await Bun.write(tarballPath, await response.arrayBuffer()); @@ -130,7 +186,7 @@ async function vendorNpmVersion(version: string): Promise { ); verifyVendoredCli(); - console.log(`Vendored @qwen-code/qwen-code@${version} into ${vendorDir}`); + console.log(`Vendored ${qwenCodePackageName}@${version} into ${vendorDir}`); } finally { rmSync(tempDir, { recursive: true, force: true }); }