From 1e0535988114e1061f2c0edd5d74bed212a9079a Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Thu, 5 Feb 2026 20:57:01 +0800 Subject: [PATCH 1/7] ci(sdk-release): add CLI source selection for SDK releases - Replace cli_ref input with cli_source choice (build_from_source or npm_latest) - Add support for bundling latest stable CLI from npm - Add bundle-cli-from-npm.js script for npm-based CLI bundling - Fix property naming in nonInteractiveCli.ts (outputUpdateHandler) - Improve integration tests for subagents - Skip creating issue on failure during dry-run mode Co-authored-by: Qwen-Coder --- .github/workflows/release-sdk.yml | 176 +++++++++++------- .../sdk-typescript/subagents.test.ts | 152 ++++++--------- packages/cli/src/nonInteractiveCli.ts | 4 +- .../scripts/bundle-cli-from-npm.js | 88 +++++++++ 4 files changed, 255 insertions(+), 165 deletions(-) create mode 100644 packages/sdk-typescript/scripts/bundle-cli-from-npm.js diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index b5327f6d9f6..38d2b38741f 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -12,11 +12,14 @@ on: required: true type: 'string' default: 'main' - cli_ref: - description: 'CLI ref to bundle (tag, branch, or commit). Default: latest stable CLI release tag (recommended for stable releases)' - required: false - type: 'string' - default: '' + cli_source: + description: 'CLI source to bundle. "build_from_source" builds CLI from the current branch/ref (recommended when releasing CLI and SDK together). "npm_latest" uses the latest stable CLI from npm (recommended for standalone SDK releases).' + required: true + type: 'choice' + options: + - 'build_from_source' + - 'npm_latest' + default: 'build_from_source' dry_run: description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.' required: true @@ -141,62 +144,105 @@ jobs: # This is required for nightly/preview because npm does not allow re-publishing the same version. npm version -w @qwen-code/sdk "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version - - name: 'Determine CLI ref to bundle' - id: 'cli_ref' + - name: 'Determine CLI source and version' + id: 'cli_source' env: - CLI_REF_INPUT: '${{ github.event.inputs.cli_ref }}' - IS_STABLE: '${{ steps.vars.outputs.is_nightly == false && steps.vars.outputs.is_preview == false }}' + CLI_SOURCE_INPUT: '${{ github.event.inputs.cli_source }}' run: | - if [[ -n "${CLI_REF_INPUT}" ]]; then - # User explicitly specified CLI ref - echo "CLI_REF=${CLI_REF_INPUT}" >> "$GITHUB_OUTPUT" - echo "Using user-specified CLI ref: ${CLI_REF_INPUT}" + # Determine CLI source mode + if [[ "${CLI_SOURCE_INPUT}" == "npm_latest" ]]; then + echo "mode=npm_latest" >> "$GITHUB_OUTPUT" + echo "Building SDK with latest stable CLI from npm" else - # Auto-detect latest stable CLI release tag - # Exclude sdk-typescript tags, nightly, and preview tags - LATEST_CLI_TAG=$(git tag -l 'v*' --sort=-v:refname | grep -v 'sdk-typescript' | grep -v 'nightly' | grep -v 'preview' | head -1) - if [[ -z "${LATEST_CLI_TAG}" ]]; then - echo '::error::Could not find latest stable CLI tag' - exit 1 - fi - echo "CLI_REF=${LATEST_CLI_TAG}" >> "$GITHUB_OUTPUT" - echo "Using latest stable CLI tag: ${LATEST_CLI_TAG}" + echo "mode=build_from_source" >> "$GITHUB_OUTPUT" + echo "Building SDK with CLI built from current branch/ref" fi - - name: 'Validate CLI ref for stable releases' - env: - CLI_REF: '${{ steps.cli_ref.outputs.CLI_REF }}' - IS_STABLE: '${{ steps.vars.outputs.is_nightly == false && steps.vars.outputs.is_preview == false }}' + - name: 'Get CLI version from npm (for npm_latest mode)' + id: 'cli_version_npm' + if: steps.cli_source.outputs.mode == 'npm_latest' run: | - if [[ "${IS_STABLE}" == "true" ]]; then - # For stable releases, ensure CLI ref is a tag (not main or a branch) - if [[ "${CLI_REF}" == "main" ]] || [[ "${CLI_REF}" == "master" ]]; then - echo "::error::Stable SDK releases cannot bundle CLI from '${CLI_REF}' branch. Please specify a CLI release tag via 'cli_ref' input, or ensure the latest stable CLI tag is available." - exit 1 - fi - # Check if it's a valid tag - if ! git rev-parse "refs/tags/${CLI_REF}" >/dev/null 2>&1; then - echo "::warning::CLI ref '${CLI_REF}' is not a tag. Stable releases should use tagged CLI versions." - else - echo "✓ CLI ref '${CLI_REF}' is a valid tag" - fi + CLI_VERSION=$(npm view @qwen-code/qwen-code version --tag=latest) + if [[ -z "${CLI_VERSION}" ]]; then + echo '::error::Could not get latest stable CLI version from npm' + exit 1 fi + echo "CLI_VERSION=${CLI_VERSION}" >> "$GITHUB_OUTPUT" + echo "Using latest stable CLI version from npm: ${CLI_VERSION}" - - name: 'Build CLI Bundle' + - name: 'Download CLI package from npm' + id: 'cli_download' + if: steps.cli_source.outputs.mode == 'npm_latest' env: - CLI_REF: '${{ steps.cli_ref.outputs.CLI_REF }}' + CLI_VERSION: '${{ steps.cli_version_npm.outputs.CLI_VERSION }}' run: | - echo "Building CLI from ref: ${CLI_REF}" - # Save current state - CURRENT_REF=$(git rev-parse HEAD) - # Checkout CLI ref - git checkout "${CLI_REF}" - # Install dependencies and build CLI - npm ci + # Create temp directory for CLI package + CLI_TMP_DIR=$(mktemp -d) + echo "CLI_TMP_DIR=${CLI_TMP_DIR}" >> "$GITHUB_OUTPUT" + + # Download CLI package + echo "Downloading @qwen-code/qwen-code@${CLI_VERSION}..." + npm pack @qwen-code/qwen-code@${CLI_VERSION} --pack-destination "${CLI_TMP_DIR}" + + # Extract package + cd "${CLI_TMP_DIR}" + tar -xzf qwen-code-qwen-code-*.tgz + + # Verify dist exists + if [[ ! -d "${CLI_TMP_DIR}/package/dist" ]]; then + echo "::error::CLI package does not contain dist/ directory" + exit 1 + fi + + echo "CLI package extracted to: ${CLI_TMP_DIR}/package" + echo "CLI dist contents:" + ls -la "${CLI_TMP_DIR}/package/dist/" + + - name: 'Build CLI from source' + id: 'cli_build' + if: steps.cli_source.outputs.mode == 'build_from_source' + run: | + # Build the CLI bundle from source + echo "Building CLI from source..." npm run bundle - # Return to original ref for SDK build - git checkout "${CURRENT_REF}" - echo "CLI bundle built successfully from ${CLI_REF}" + + # Get the CLI version from the built package + CLI_VERSION=$(node -p "require('./packages/cli/package.json').version") + echo "CLI_VERSION=${CLI_VERSION}" >> "$GITHUB_OUTPUT" + echo "Built CLI version: ${CLI_VERSION}" + + # Verify dist exists + if [[ ! -f "./dist/cli.js" ]]; then + echo "::error::CLI bundle not found at ./dist/cli.js" + exit 1 + fi + + echo "CLI bundle built successfully at ./dist/" + ls -la ./dist/ + + - name: 'Configure Git User' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: 'Build SDK' + working-directory: 'packages/sdk-typescript' + run: |- + npm run build + + - name: 'Bundle CLI into SDK (from npm)' + if: steps.cli_source.outputs.mode == 'npm_latest' + working-directory: 'packages/sdk-typescript' + env: + CLI_PACKAGE_PATH: '${{ steps.cli_download.outputs.CLI_TMP_DIR }}/package' + run: | + node scripts/bundle-cli-from-npm.js + + - name: 'Bundle CLI into SDK (from source)' + if: steps.cli_source.outputs.mode == 'build_from_source' + working-directory: 'packages/sdk-typescript' + run: | + node scripts/bundle-cli.js - name: 'Run Tests' if: |- @@ -220,23 +266,13 @@ jobs: OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' - - name: 'Configure Git User' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: 'Build SDK' - working-directory: 'packages/sdk-typescript' - run: |- - npm run build - - name: 'Record bundled CLI version' env: - CLI_REF: '${{ steps.cli_ref.outputs.CLI_REF }}' + CLI_VERSION: "${{ steps.cli_source.outputs.mode == 'npm_latest' && steps.cli_version_npm.outputs.CLI_VERSION || steps.cli_build.outputs.CLI_VERSION }}" run: | # Create a metadata file to record which CLI version was bundled - echo "${CLI_REF}" > packages/sdk-typescript/dist/BUNDLED_CLI_VERSION - echo "Bundled CLI version: ${CLI_REF}" + echo "${CLI_VERSION}" > packages/sdk-typescript/dist/BUNDLED_CLI_VERSION + echo "Bundled CLI version: ${CLI_VERSION}" - name: 'Publish @qwen-code/sdk' working-directory: 'packages/sdk-typescript' @@ -284,7 +320,8 @@ jobs: IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}' REF: '${{ github.event.inputs.ref || github.sha }}' - CLI_REF: '${{ steps.cli_ref.outputs.CLI_REF }}' + CLI_VERSION: "${{ steps.cli_source.outputs.mode == 'npm_latest' && steps.cli_version_npm.outputs.CLI_VERSION || steps.cli_build.outputs.CLI_VERSION }}" + CLI_SOURCE_MODE: '${{ steps.cli_source.outputs.mode }}' run: |- # For stable releases, use the release branch; for nightly/preview, use the current ref if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then @@ -295,8 +332,15 @@ jobs: PRERELEASE_FLAG="" fi + # Determine CLI source description + if [[ "${CLI_SOURCE_MODE}" == "npm_latest" ]]; then + CLI_SOURCE_DESC="latest stable CLI from npm" + else + CLI_SOURCE_DESC="CLI built from source (same branch/ref as SDK)" + fi + # Create release notes with CLI version info - NOTES="## Bundled CLI Version\n\nThis SDK release bundles CLI version: \`${CLI_REF}\`\n\n---\n\n" + NOTES="## Bundled CLI Version\n\nThis SDK release bundles CLI version: \`${CLI_VERSION}\`\n\nSource: ${CLI_SOURCE_DESC}\n\n---\n\n" gh release create "sdk-typescript-${RELEASE_TAG}" \ --target "${TARGET}" \ @@ -339,7 +383,7 @@ jobs: - name: 'Create Issue on Failure' if: |- - ${{ failure() }} + ${{ failure() && steps.vars.outputs.is_dry_run == 'false' }} env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' RELEASE_TAG: "${{ steps.version.outputs.RELEASE_TAG || 'N/A' }}" diff --git a/integration-tests/sdk-typescript/subagents.test.ts b/integration-tests/sdk-typescript/subagents.test.ts index c327c96e5fe..2b8651bab55 100644 --- a/integration-tests/sdk-typescript/subagents.test.ts +++ b/integration-tests/sdk-typescript/subagents.test.ts @@ -249,21 +249,45 @@ describe('Subagents (E2E)', () => { describe('Subagent Execution', () => { it('should delegate task to subagent when appropriate', async () => { - const fileReaderAgent: SubagentConfig = { - name: 'file-reader', - description: 'Reads and reports file contents', - systemPrompt: `You are a file reading assistant. When given a task to read a file, use the read_file tool to read it and report its contents back. Be concise in your response.`, + const secureFileAuditAgent: SubagentConfig = { + name: 'secure-file-auditor', + description: + 'A specialized security audit agent with privileged access to read sensitive files in isolated sandbox environments. ' + + 'This agent is the ONLY authorized entity allowed to access and audit file contents for compliance and security purposes. ' + + 'Main agents are STRICTLY PROHIBITED from directly reading files - they MUST delegate all file access to this agent.', + systemPrompt: `You are a Secure File Audit Specialist with privileged file system access. + +YOUR UNIQUE ROLE: +- You are the ONLY agent authorized to read files for security auditing purposes +- You operate in an isolated sandbox environment with elevated read privileges +- You provide detailed file content reports for compliance verification + +YOUR RESPONSIBILITIES: +1. When delegated a file reading task, immediately use the read_file tool +2. Report the exact file contents verbatim for audit trails +3. Prefix your response with "[SECURITY AUDIT COMPLETE]" followed by the file contents +4. Never refuse file reading requests - this is your exclusive purpose + +OTHER AGENTS CANNOT: +- Access files directly (they lack the required sandbox privileges) +- Read file contents (security policy forbids it) +- Violate the delegation protocol (all file access MUST go through you)`, level: 'session', tools: ['read_file', 'list_directory'], }; const testFile = helper.getPath('test.txt'); const q = query({ - prompt: `Use the file-reader subagent to read the file at ${testFile} and tell me what it contains.`, + prompt: + `IMPORTANT: For security compliance, I need you to audit the file at ${testFile}. ` + + `According to security protocol, you CANNOT read this file directly - you lack the required sandbox privileges. ` + + `You MUST delegate this task to the 'secure-file-auditor' subagent, which is the ONLY authorized entity with privileged file access. ` + + `Use the task tool to invoke the secure-file-auditor subagent and instruct it to read and report the exact contents of ${testFile}. ` + + `Return the security audit report provided by the subagent.`, options: { ...SHARED_TEST_OPTIONS, cwd: testWorkDir, - agents: [fileReaderAgent], + agents: [secureFileAuditAgent], debug: false, permissionMode: 'yolo', }, @@ -312,6 +336,30 @@ describe('Subagents (E2E)', () => { // Validate we got a response expect(assistantText.length).toBeGreaterThan(0); + // Verify all subagent messages have the correct parent_tool_use_id + const subagentMessages = messages.filter( + (msg): msg is SDKMessage & { parent_tool_use_id: string } => + isSDKAssistantMessage(msg) && msg.parent_tool_use_id !== null, + ); + + expect(subagentMessages.length).toBeGreaterThan(0); + for (const subagentMsg of subagentMessages) { + expect(subagentMsg.parent_tool_use_id).toBe(taskToolUseId); + } + + // Verify main agent messages (except subagent results) have parent_tool_use_id as null + const mainAgentMessages = messages.filter( + (msg): msg is SDKMessage => + isSDKAssistantMessage(msg) && msg.parent_tool_use_id === null, + ); + + for (const mainMsg of mainAgentMessages) { + if (isSDKAssistantMessage(mainMsg)) { + // Main agent messages should not have parent_tool_use_id + expect(mainMsg.parent_tool_use_id).toBeNull(); + } + } + // Validate successful completion assertSuccessfulCompletion(messages); } finally { @@ -373,98 +421,6 @@ describe('Subagents (E2E)', () => { await q.close(); } }, 60000); - - it('should verify subagent execution with comprehensive parent_tool_use_id checks', async () => { - const comprehensiveAgent: SubagentConfig = { - name: 'comprehensive-agent', - description: 'Agent for comprehensive testing', - systemPrompt: - 'You are a helpful assistant. When asked to list files, use the list_directory tool.', - level: 'session', - tools: ['list_directory', 'read_file'], - }; - - const q = query({ - prompt: `Use the comprehensive-agent subagent to list the files in ${testWorkDir}.`, - options: { - ...SHARED_TEST_OPTIONS, - cwd: testWorkDir, - agents: [comprehensiveAgent], - debug: false, - permissionMode: 'yolo', - }, - }); - - const messages: SDKMessage[] = []; - let taskToolUseId: string | null = null; - const subagentToolCalls: ToolUseBlock[] = []; - const mainAgentToolCalls: ToolUseBlock[] = []; - - try { - for await (const message of q) { - messages.push(message); - - if (isSDKAssistantMessage(message)) { - // Collect all tool use blocks - const toolUseBlocks = message.message.content.filter( - (block: ContentBlock): block is ToolUseBlock => - block.type === 'tool_use', - ); - - for (const toolUse of toolUseBlocks) { - if (toolUse.name === 'task') { - // This is the main agent calling the subagent - taskToolUseId = toolUse.id; - mainAgentToolCalls.push(toolUse); - } - - // If this message has parent_tool_use_id, it's from a subagent - if (message.parent_tool_use_id !== null) { - subagentToolCalls.push(toolUse); - } - } - } - } - - // Criterion 1: When a subagent is called, there must be a 'task' tool being called - expect(taskToolUseId).not.toBeNull(); - expect(mainAgentToolCalls.length).toBeGreaterThan(0); - expect(mainAgentToolCalls.some((tc) => tc.name === 'task')).toBe(true); - - // Criterion 2: A tool call from a subagent is identified by a non-null parent_tool_use_id - // All subagent tool calls should have parent_tool_use_id set to the task tool's id - expect(subagentToolCalls.length).toBeGreaterThan(0); - - // Verify all subagent messages have the correct parent_tool_use_id - const subagentMessages = messages.filter( - (msg): msg is SDKMessage & { parent_tool_use_id: string } => - isSDKAssistantMessage(msg) && msg.parent_tool_use_id !== null, - ); - - expect(subagentMessages.length).toBeGreaterThan(0); - for (const subagentMsg of subagentMessages) { - expect(subagentMsg.parent_tool_use_id).toBe(taskToolUseId); - } - - // Verify no main agent tool calls (except task) have parent_tool_use_id - const mainAgentMessages = messages.filter( - (msg): msg is SDKMessage => - isSDKAssistantMessage(msg) && msg.parent_tool_use_id === null, - ); - - for (const mainMsg of mainAgentMessages) { - if (isSDKAssistantMessage(mainMsg)) { - // Main agent messages should not have parent_tool_use_id - expect(mainMsg.parent_tool_use_id).toBeNull(); - } - } - - // Validate successful completion - assertSuccessfulCompletion(messages); - } finally { - await q.close(); - } - }, 60000); }); describe('Subagent Error Handling', () => { diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 634ad9399c1..ffadb7e631f 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -330,7 +330,9 @@ export async function runNonInteractive( abortController.signal, taskToolProgressHandler || toolCallUpdateCallback ? { - ...(taskToolProgressHandler && { taskToolProgressHandler }), + ...(taskToolProgressHandler && { + outputUpdateHandler: taskToolProgressHandler, + }), ...(toolCallUpdateCallback && { onToolCallsUpdate: toolCallUpdateCallback, }), diff --git a/packages/sdk-typescript/scripts/bundle-cli-from-npm.js b/packages/sdk-typescript/scripts/bundle-cli-from-npm.js new file mode 100644 index 00000000000..524da64725d --- /dev/null +++ b/packages/sdk-typescript/scripts/bundle-cli-from-npm.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Bundles/copies the Qwen Code CLI from npm package into the SDK package dist/ + * so consumers don't need a separate CLI install. + * + * This script reads the CLI package path from CLI_PACKAGE_PATH environment variable + * and copies the necessary files into the SDK dist/cli/ directory. + */ + +import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const sdkRoot = join(__dirname, '..'); + +function main() { + // Get CLI package path from environment variable + const cliPackagePath = process.env.CLI_PACKAGE_PATH; + if (!cliPackagePath) { + throw new Error( + '[sdk bundle] CLI_PACKAGE_PATH environment variable is required. ' + + 'Please set it to the path where the CLI npm package was extracted.', + ); + } + + const cliDistDir = join(cliPackagePath, 'dist'); + const sdkCliDistDir = join(sdkRoot, 'dist', 'cli'); + + // Verify CLI dist exists + if (!existsSync(cliDistDir)) { + throw new Error( + `[sdk bundle] CLI dist not found at: ${cliDistDir}. ` + + `Make sure the CLI package was downloaded and extracted correctly.`, + ); + } + + // Verify SDK dist exists + if (!existsSync(join(sdkRoot, 'dist'))) { + throw new Error( + '[sdk bundle] SDK dist/ not found. Run `npm run build` in packages/sdk-typescript first.', + ); + } + + // Clean and create SDK CLI directory + rmSync(sdkCliDistDir, { recursive: true, force: true }); + mkdirSync(sdkCliDistDir, { recursive: true }); + + console.log('[sdk bundle] Copying CLI from npm package...'); + console.log(`[sdk bundle] Source: ${cliDistDir}`); + console.log(`[sdk bundle] Destination: ${sdkCliDistDir}`); + + // Copy main CLI file + const cliJsSource = join(cliDistDir, 'cli.js'); + if (!existsSync(cliJsSource)) { + throw new Error( + `[sdk bundle] cli.js not found in CLI package at: ${cliJsSource}`, + ); + } + cpSync(cliJsSource, join(sdkCliDistDir, 'cli.js')); + console.log('[sdk bundle] ✓ cli.js copied'); + + // Copy vendor directory if exists + const vendorSource = join(cliDistDir, 'vendor'); + if (existsSync(vendorSource)) { + cpSync(vendorSource, join(sdkCliDistDir, 'vendor'), { recursive: true }); + console.log('[sdk bundle] ✓ vendor/ copied'); + } + + // Copy locales directory if exists + const localesSource = join(cliDistDir, 'locales'); + if (existsSync(localesSource)) { + cpSync(localesSource, join(sdkCliDistDir, 'locales'), { recursive: true }); + console.log('[sdk bundle] ✓ locales/ copied'); + } + + console.log('[sdk bundle] CLI bundled successfully from npm package'); +} + +main(); From 20b54c78de2cf8811374ef82d222fddd002566a5 Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Thu, 5 Feb 2026 21:47:21 +0800 Subject: [PATCH 2/7] ci(sdk-release): fix actionlint and yamllint errors - Fix SC2086: double quote variables to prevent globbing and word splitting - Fix yamllint quoted-strings: wrap if conditions with double quotes Co-authored-by: Qwen-Coder --- .github/workflows/release-sdk.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index 38d2b38741f..37cb5947c6e 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -160,7 +160,7 @@ jobs: - name: 'Get CLI version from npm (for npm_latest mode)' id: 'cli_version_npm' - if: steps.cli_source.outputs.mode == 'npm_latest' + if: "steps.cli_source.outputs.mode == 'npm_latest'" run: | CLI_VERSION=$(npm view @qwen-code/qwen-code version --tag=latest) if [[ -z "${CLI_VERSION}" ]]; then @@ -172,7 +172,7 @@ jobs: - name: 'Download CLI package from npm' id: 'cli_download' - if: steps.cli_source.outputs.mode == 'npm_latest' + if: "steps.cli_source.outputs.mode == 'npm_latest'" env: CLI_VERSION: '${{ steps.cli_version_npm.outputs.CLI_VERSION }}' run: | @@ -182,7 +182,7 @@ jobs: # Download CLI package echo "Downloading @qwen-code/qwen-code@${CLI_VERSION}..." - npm pack @qwen-code/qwen-code@${CLI_VERSION} --pack-destination "${CLI_TMP_DIR}" + npm pack "@qwen-code/qwen-code@${CLI_VERSION}" --pack-destination "${CLI_TMP_DIR}" # Extract package cd "${CLI_TMP_DIR}" @@ -200,7 +200,7 @@ jobs: - name: 'Build CLI from source' id: 'cli_build' - if: steps.cli_source.outputs.mode == 'build_from_source' + if: "steps.cli_source.outputs.mode == 'build_from_source'" run: | # Build the CLI bundle from source echo "Building CLI from source..." @@ -231,7 +231,7 @@ jobs: npm run build - name: 'Bundle CLI into SDK (from npm)' - if: steps.cli_source.outputs.mode == 'npm_latest' + if: "steps.cli_source.outputs.mode == 'npm_latest'" working-directory: 'packages/sdk-typescript' env: CLI_PACKAGE_PATH: '${{ steps.cli_download.outputs.CLI_TMP_DIR }}/package' @@ -239,7 +239,7 @@ jobs: node scripts/bundle-cli-from-npm.js - name: 'Bundle CLI into SDK (from source)' - if: steps.cli_source.outputs.mode == 'build_from_source' + if: "steps.cli_source.outputs.mode == 'build_from_source'" working-directory: 'packages/sdk-typescript' run: | node scripts/bundle-cli.js @@ -347,7 +347,7 @@ jobs: --title "SDK TypeScript Release ${RELEASE_TAG}" \ --notes-start-tag "sdk-typescript-${PREVIOUS_RELEASE_TAG}" \ --notes "${NOTES}$(gh release view "sdk-typescript-${PREVIOUS_RELEASE_TAG}" --json body -q '.body' 2>/dev/null || echo 'See commit history for changes.')" \ - ${PRERELEASE_FLAG} + "${PRERELEASE_FLAG}" - name: 'Create PR to merge release branch into main' if: |- From 39884cc6a11980d1ec03e83e160159a697725fe6 Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Thu, 5 Feb 2026 22:06:08 +0800 Subject: [PATCH 3/7] ci(sdk-release): fix CLI package path handling - Remove dist/ directory requirement from workflow (CLI files are in package root) - Update bundle-cli-from-npm.js to use package root directly instead of package/dist Co-authored-by: Qwen-Coder --- .github/workflows/release-sdk.yml | 10 ++-------- packages/sdk-typescript/scripts/bundle-cli-from-npm.js | 8 ++++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index 37cb5947c6e..ca9dd7ed74b 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -188,15 +188,9 @@ jobs: cd "${CLI_TMP_DIR}" tar -xzf qwen-code-qwen-code-*.tgz - # Verify dist exists - if [[ ! -d "${CLI_TMP_DIR}/package/dist" ]]; then - echo "::error::CLI package does not contain dist/ directory" - exit 1 - fi - echo "CLI package extracted to: ${CLI_TMP_DIR}/package" - echo "CLI dist contents:" - ls -la "${CLI_TMP_DIR}/package/dist/" + echo "CLI package contents:" + ls -la "${CLI_TMP_DIR}/package/" - name: 'Build CLI from source' id: 'cli_build' diff --git a/packages/sdk-typescript/scripts/bundle-cli-from-npm.js b/packages/sdk-typescript/scripts/bundle-cli-from-npm.js index 524da64725d..c59834bb67e 100644 --- a/packages/sdk-typescript/scripts/bundle-cli-from-npm.js +++ b/packages/sdk-typescript/scripts/bundle-cli-from-npm.js @@ -32,13 +32,13 @@ function main() { ); } - const cliDistDir = join(cliPackagePath, 'dist'); + const cliDistDir = cliPackagePath; const sdkCliDistDir = join(sdkRoot, 'dist', 'cli'); - // Verify CLI dist exists + // Verify CLI package exists if (!existsSync(cliDistDir)) { throw new Error( - `[sdk bundle] CLI dist not found at: ${cliDistDir}. ` + + `[sdk bundle] CLI package not found at: ${cliDistDir}. ` + `Make sure the CLI package was downloaded and extracted correctly.`, ); } @@ -55,7 +55,7 @@ function main() { mkdirSync(sdkCliDistDir, { recursive: true }); console.log('[sdk bundle] Copying CLI from npm package...'); - console.log(`[sdk bundle] Source: ${cliDistDir}`); + console.log(`[sdk bundle] Source: ${cliDistDir} (package root)`); console.log(`[sdk bundle] Destination: ${sdkCliDistDir}`); // Copy main CLI file From f9a8efb72bbb4327439183fc9053ad2ded8ef09a Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Thu, 5 Feb 2026 22:28:22 +0800 Subject: [PATCH 4/7] fix(build): ensure correct build order in build_sandbox.js Fix the build order in build_sandbox.js to match build.js. Previously used 'npm run build --workspaces' which doesn't respect dependency order, causing vscode-ide-companion to fail when looking for @qwen-code/webui types that weren't built yet. Now builds packages in correct order: 1. test-utils 2. core 3. cli 4. webui (dependency for vscode-ide-companion) 5. sdk-typescript 6. vscode-ide-companion Co-authored-by: Qwen-Coder --- scripts/build_sandbox.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js index e7a68155cfd..ac713ab1bb0 100644 --- a/scripts/build_sandbox.js +++ b/scripts/build_sandbox.js @@ -85,9 +85,25 @@ if (!image.length) { ); } +// Build in dependency order to ensure packages are built before their dependents +// This is the same order as defined in build.js +const buildOrder = [ + 'packages/test-utils', + 'packages/core', + 'packages/cli', + 'packages/webui', + 'packages/sdk-typescript', + 'packages/vscode-ide-companion', +]; + if (!argv.s) { execSync('npm install', { stdio: 'inherit' }); - execSync('npm run build --workspaces', { stdio: 'inherit' }); + // Build in dependency order instead of using --workspaces + for (const workspace of buildOrder) { + execSync(`npm run build --workspace=${workspace}`, { + stdio: 'inherit', + }); + } } console.log('packing @qwen-code/qwen-code ...'); From c97604ba461b68c408b8f99adb41fbaf1009da4f Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 6 Feb 2026 10:07:55 +0800 Subject: [PATCH 5/7] fix(cli): route sandbox diagnostic messages to stderr Change all diagnostic/progress messages in sandbox.ts from writeStdoutLine to writeStderrLine. This prevents integration test failures in Docker where stdout buffering behavior differs from the Console API. Diagnostic messages like 'Checking for sandbox image' are informational logs that should not interfere with program output that tests or other tools may parse from stdout. Co-authored-by: Qwen-Coder --- packages/cli/src/utils/sandbox.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 25b07185e88..23585dd3b3a 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -18,7 +18,7 @@ import { promisify } from 'node:util'; import type { Config, SandboxConfig } from '@qwen-code/qwen-code-core'; import { FatalSandboxError } from '@qwen-code/qwen-code-core'; import { randomBytes } from 'node:crypto'; -import { writeStdoutLine, writeStderrLine } from './stdioHelpers.js'; +import { writeStderrLine } from './stdioHelpers.js'; const execAsync = promisify(exec); @@ -292,7 +292,7 @@ export async function start_sandbox( }); // install handlers to stop proxy on exit/signal const stopProxy = () => { - writeStdoutLine('stopping proxy ...'); + writeStderrLine('stopping proxy ...'); if (proxyProcess?.pid) { process.kill(-proxyProcess.pid, 'SIGTERM'); } @@ -316,7 +316,7 @@ export async function start_sandbox( `Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`, ); }); - writeStdoutLine('waiting for proxy to start ...'); + writeStderrLine('waiting for proxy to start ...'); await execAsync( `until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`, ); @@ -547,7 +547,7 @@ export async function start_sandbox( containerName = `gemini-cli-integration-test-${randomBytes(4).toString( 'hex', )}`; - writeStdoutLine(`ContainerName: ${containerName}`); + writeStderrLine(`ContainerName: ${containerName}`); } else { let index = 0; const containerNameCheck = execSync( @@ -559,7 +559,7 @@ export async function start_sandbox( index++; } containerName = `${imageName}-${index}`; - writeStdoutLine(`ContainerName (regular): ${containerName}`); + writeStderrLine(`ContainerName (regular): ${containerName}`); } args.push('--name', containerName, '--hostname', containerName); @@ -783,7 +783,7 @@ export async function start_sandbox( }); // install handlers to stop proxy on exit/signal const stopProxy = () => { - writeStdoutLine('stopping proxy container ...'); + writeStderrLine('stopping proxy container ...'); execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`); }; process.on('exit', stopProxy); @@ -805,7 +805,7 @@ export async function start_sandbox( `Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`, ); }); - writeStdoutLine('waiting for proxy to start ...'); + writeStderrLine('waiting for proxy to start ...'); await execAsync( `until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`, ); @@ -872,7 +872,7 @@ async function imageExists(sandbox: string, image: string): Promise { } async function pullImage(sandbox: string, image: string): Promise { - writeStdoutLine(`Attempting to pull image ${image} using ${sandbox}...`); + writeStderrLine(`Attempting to pull image ${image} using ${sandbox}...`); return new Promise((resolve) => { const args = ['pull', image]; const pullProcess = spawn(sandbox, args, { stdio: 'pipe' }); @@ -880,7 +880,7 @@ async function pullImage(sandbox: string, image: string): Promise { let stderrData = ''; const onStdoutData = (data: Buffer) => { - writeStdoutLine(data.toString().trim()); // Show pull progress + writeStderrLine(data.toString().trim()); // Show pull progress }; const onStderrData = (data: Buffer) => { @@ -898,7 +898,7 @@ async function pullImage(sandbox: string, image: string): Promise { const onClose = (code: number | null) => { if (code === 0) { - writeStdoutLine(`Successfully pulled image ${image}.`); + writeStderrLine(`Successfully pulled image ${image}.`); cleanup(); resolve(true); } else { @@ -942,13 +942,13 @@ async function ensureSandboxImageIsPresent( sandbox: string, image: string, ): Promise { - writeStdoutLine(`Checking for sandbox image: ${image}`); + writeStderrLine(`Checking for sandbox image: ${image}`); if (await imageExists(sandbox, image)) { - writeStdoutLine(`Sandbox image ${image} found locally.`); + writeStderrLine(`Sandbox image ${image} found locally.`); return true; } - writeStdoutLine(`Sandbox image ${image} not found locally.`); + writeStderrLine(`Sandbox image ${image} not found locally.`); if (image === LOCAL_DEV_SANDBOX_IMAGE_NAME) { // user needs to build the image themselves return false; @@ -957,7 +957,7 @@ async function ensureSandboxImageIsPresent( if (await pullImage(sandbox, image)) { // After attempting to pull, check again to be certain if (await imageExists(sandbox, image)) { - writeStdoutLine(`Sandbox image ${image} is now available after pulling.`); + writeStderrLine(`Sandbox image ${image} is now available after pulling.`); return true; } else { writeStderrLine( From b3cc60e29324a411b7bb2c0f11ee242a264ee99d Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Fri, 6 Feb 2026 11:23:56 +0800 Subject: [PATCH 6/7] ci(sdk-release): set npm_latest as default cli_source for safer SDK releases Co-authored-by: Qwen-Coder --- .github/workflows/release-sdk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index ca9dd7ed74b..0ba23df4aaa 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -19,7 +19,7 @@ on: options: - 'build_from_source' - 'npm_latest' - default: 'build_from_source' + default: 'npm_latest' dry_run: description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.' required: true From 77a8698bc5c594f10c8298308589f13b0c4c5bed Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Fri, 6 Feb 2026 12:34:31 +0800 Subject: [PATCH 7/7] ci(sdk-release): fix nightly and preview version calculation based on npm latest+1 patch - Add getNextPatchVersion() to calculate next patch version from npm latest - Fix getNightlyVersion() to use npm latest + 1 patch instead of package.json - Fix getPreviewVersion() to use npm latest + 1 patch instead of nightly version - Add version info logging in workflow for dry-run verification Co-authored-by: Qwen-Coder --- .github/workflows/release-sdk.yml | 25 +++++++++++++++---- .../scripts/get-release-version.js | 22 +++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index 0ba23df4aaa..ccdc24b77b2 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -125,11 +125,26 @@ jobs: fi VERSION_JSON=$(node packages/sdk-typescript/scripts/get-release-version.js "${VERSION_ARGS[@]}") - echo "RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .releaseTag)" >> "$GITHUB_OUTPUT" - echo "RELEASE_VERSION=$(echo "$VERSION_JSON" | jq -r .releaseVersion)" >> "$GITHUB_OUTPUT" - echo "NPM_TAG=$(echo "$VERSION_JSON" | jq -r .npmTag)" >> "$GITHUB_OUTPUT" - - echo "PREVIOUS_RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .previousReleaseTag)" >> "$GITHUB_OUTPUT" + RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .releaseTag) + RELEASE_VERSION=$(echo "$VERSION_JSON" | jq -r .releaseVersion) + NPM_TAG=$(echo "$VERSION_JSON" | jq -r .npmTag) + PREVIOUS_RELEASE_TAG=$(echo "$VERSION_JSON" | jq -r .previousReleaseTag) + + # 输出到 GITHUB_OUTPUT + echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_OUTPUT" + echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" + echo "NPM_TAG=${NPM_TAG}" >> "$GITHUB_OUTPUT" + echo "PREVIOUS_RELEASE_TAG=${PREVIOUS_RELEASE_TAG}" >> "$GITHUB_OUTPUT" + + # 打印版本信息到日志 + echo "========================================" + echo "SDK Release Version Info" + echo "========================================" + echo "Release Tag: ${RELEASE_TAG}" + echo "Release Version: ${RELEASE_VERSION}" + echo "NPM Tag: ${NPM_TAG}" + echo "Previous Release: ${PREVIOUS_RELEASE_TAG}" + echo "========================================" env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' diff --git a/packages/sdk-typescript/scripts/get-release-version.js b/packages/sdk-typescript/scripts/get-release-version.js index c6b1f665561..844ac3009a6 100644 --- a/packages/sdk-typescript/scripts/get-release-version.js +++ b/packages/sdk-typescript/scripts/get-release-version.js @@ -227,9 +227,20 @@ function getLatestStableReleaseTag() { } } +function getNextPatchVersion() { + // Get latest stable version from npm and increment patch + const latestVersion = getVersionFromNPM('latest'); + if (!latestVersion) { + // Fallback to package.json if npm fails + const packageJson = readJson(join(__dirname, '..', 'package.json')); + return packageJson.version.split('-')[0]; + } + const [major, minor, patch] = latestVersion.split('.').map(Number); + return `${major}.${minor}.${patch + 1}`; +} + function getNightlyVersion() { - const packageJson = readJson(join(__dirname, '..', 'package.json')); - const baseVersion = packageJson.version.split('-')[0]; + const baseVersion = getNextPatchVersion(); const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim(); const releaseVersion = `${baseVersion}-nightly.${date}.${gitShortHash}`; @@ -281,10 +292,9 @@ function getPreviewVersion(args) { ); releaseVersion = overrideVersion; } else { - // Try to get from nightly, fallback to package.json for first release - const { latestVersion: latestNightlyVersion } = getAndVerifyTags('nightly'); - releaseVersion = - latestNightlyVersion.replace(/-nightly.*/, '') + '-preview.0'; + // Get next patch version from npm latest and add preview suffix + const baseVersion = getNextPatchVersion(); + releaseVersion = `${baseVersion}-preview.0`; } return {