Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e1ffaec
feat: create draft framework for cli & sdk
Mingholy Nov 23, 2025
d76cdf1
feat: sdk subagent support
Mingholy Nov 25, 2025
ad9ba91
refactor: clean up exports in sdk-typescript index file
Mingholy Nov 25, 2025
ac6aecb
refactor: update test structure and clean up unused code in cli and sdk
Mingholy Nov 25, 2025
49dc84a
feat: add support for includePartialMessages option in query and tran…
Mingholy Nov 25, 2025
769a438
feat: enhance logging capabilities and update query options in sdk-ty…
Mingholy Nov 26, 2025
d76341b
chore: keep comments for queryOptions
Mingholy Nov 26, 2025
638b7bb
feat: add `allowedTools` support
Mingholy Nov 27, 2025
56957a6
refactor: rename ambiguous exported types
Mingholy Nov 27, 2025
249b141
feat: add `allowedTools` for SDK use and re-organize test setup
Mingholy Nov 28, 2025
8035be6
fix: plan mode adjustments
Mingholy Dec 1, 2025
ae7d6af
fix(test): remove unused test cases
Mingholy Dec 1, 2025
3056f8a
feat(tests): move SDK integration tests to `integration-tests` to sha…
Mingholy Dec 1, 2025
50e3a6e
fix: enhance 429 error handling and fix failed cases
Mingholy Dec 1, 2025
81c8b3e
feat: add GitHub Actions workflow for SDK release automation
Mingholy Dec 1, 2025
b1d848f
chore: update lockfile
Mingholy Dec 1, 2025
56f61bc
fix: path literals in windows
Mingholy Dec 1, 2025
839a1d9
fix: mock path for cross platform compability in test cases
Mingholy Dec 1, 2025
51b9281
chore: remove scheduled triggers from SDK release workflow
Mingholy Dec 3, 2025
c18fed5
chore: fix RELEASE_TAG fallback in workflow
Mingholy Dec 4, 2025
0630908
fix: lint error
Mingholy Dec 4, 2025
427c69b
chore: fix sdk release workflow and verified with yamllint and act
Mingholy Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions .github/workflows/release-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
name: 'Release SDK'

on:
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.'
required: false
type: 'string'
ref:
description: 'The branch or ref (full git sha) to release from.'
required: true
type: 'string'
default: 'main'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
create_nightly_release:
description: 'Auto apply the nightly release tag, input version is ignored.'
required: false
type: 'boolean'
default: false
create_preview_release:
description: 'Auto apply the preview release tag, input version is ignored.'
required: false
type: 'boolean'
default: false
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false

jobs:
release-sdk:
runs-on: 'ubuntu-latest'
environment:
name: 'production-release'
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/sdk-typescript-${{ steps.version.outputs.RELEASE_TAG }}'
if: |-
${{ github.repository == 'QwenLM/qwen-code' }}
permissions:
contents: 'write'
packages: 'write'
id-token: 'write'
issues: 'write'
outputs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'

steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0

- name: 'Set booleans for simplified logic'
env:
CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}'
CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}'
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
id: 'vars'
run: |-
is_nightly="false"
if [[ "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then
is_nightly="true"
fi
echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}"

is_preview="false"
if [[ "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then
is_preview="true"
fi
echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}"

is_dry_run="false"
if [[ "${DRY_RUN_INPUT}" == "true" ]]; then
is_dry_run="true"
fi
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"

- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: 'Install Dependencies'
run: |-
npm ci

- name: 'Get the version'
id: 'version'
run: |
VERSION_ARGS=()
if [[ "${IS_NIGHTLY}" == "true" ]]; then
VERSION_ARGS+=(--type=nightly)
elif [[ "${IS_PREVIEW}" == "true" ]]; then
VERSION_ARGS+=(--type=preview)
if [[ -n "${MANUAL_VERSION}" ]]; then
VERSION_ARGS+=("--preview_version_override=${MANUAL_VERSION}")
fi
else
VERSION_ARGS+=(--type=stable)
if [[ -n "${MANUAL_VERSION}" ]]; then
VERSION_ARGS+=("--stable_version_override=${MANUAL_VERSION}")
fi
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"
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}'
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
MANUAL_VERSION: '${{ inputs.version }}'

- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
working-directory: 'packages/sdk-typescript'
run: |
npm run test:ci
env:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
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: 'Create and switch to a release branch'
id: 'release_branch'
env:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
BRANCH_NAME="release/sdk-typescript/${RELEASE_TAG}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"

- name: 'Update package version'
working-directory: 'packages/sdk-typescript'
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version

- name: 'Commit and Conditionally Push package version'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
git add packages/sdk-typescript/package.json
if git diff --staged --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(release): sdk-typescript ${RELEASE_TAG}"
fi
if [[ "${IS_DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi

- name: 'Build SDK'
working-directory: 'packages/sdk-typescript'
run: |-
npm run build

- name: 'Configure npm for publishing'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
scope: '@qwen-code'

- name: 'Publish @qwen-code/sdk-typescript'
working-directory: 'packages/sdk-typescript'
run: |-
npm publish --access public --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'

- name: 'Create GitHub Release and Tag'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
PREVIOUS_RELEASE_TAG: '${{ steps.version.outputs.PREVIOUS_RELEASE_TAG }}'
run: |-
gh release create "sdk-typescript-${RELEASE_TAG}" \
--target "$RELEASE_BRANCH" \
--title "SDK TypeScript Release ${RELEASE_TAG}" \
--notes-start-tag "sdk-typescript-${PREVIOUS_RELEASE_TAG}" \
--generate-notes

- name: 'Create Issue on Failure'
if: |-
${{ failure() }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_TAG: "${{ steps.version.outputs.RELEASE_TAG || 'N/A' }}"
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "SDK Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
--body "The SDK release workflow failed. See the full run for details: ${DETAILS_URL}"
1 change: 0 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"--",
"-p",
"${input:prompt}",
"-y",
"--output-format",
"stream-json"
],
Expand Down
15 changes: 12 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default tseslint.config(
'bundle/**',
'package/bundle/**',
'.integration-tests/**',
'packages/**/.integration-test/**',
'dist/**',
],
},
Expand Down Expand Up @@ -150,19 +151,27 @@ export default tseslint.config(
},
},
{
files: ['packages/*/src/**/*.test.{ts,tsx}'],
files: ['packages/*/src/**/*.test.{ts,tsx}', 'packages/**/test/**/*.test.{ts,tsx}'],
plugins: {
vitest,
},
rules: {
...vitest.configs.recommended.rules,
'vitest/expect-expect': 'off',
'vitest/no-commented-out-tests': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
// extra settings for scripts that we run directly with node
{
files: ['./scripts/**/*.js', 'esbuild.config.js'],
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/*/scripts/**/*.js'],
languageOptions: {
globals: {
...globals.node,
Expand Down Expand Up @@ -229,7 +238,7 @@ export default tseslint.config(
prettierConfig,
// extra settings for scripts that we run directly with node
{
files: ['./integration-tests/**/*.js'],
files: ['./integration-tests/**/*.{js,ts,tsx}'],
languageOptions: {
globals: {
...globals.node,
Expand Down
44 changes: 40 additions & 4 deletions integration-tests/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -30,6 +30,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const integrationTestsDir = join(rootDir, '.integration-tests');
let runDir = ''; // Make runDir accessible in teardown
let sdkE2eRunDir = ''; // SDK E2E test run directory

const memoryFilePath = join(
os.homedir(),
Expand All @@ -48,14 +49,36 @@ export async function setup() {
// File doesn't exist, which is fine.
}

// Setup for CLI integration tests
runDir = join(integrationTestsDir, `${Date.now()}`);
await mkdir(runDir, { recursive: true });

// Setup for SDK E2E tests (separate directory with prefix)
sdkE2eRunDir = join(integrationTestsDir, `sdk-e2e-${Date.now()}`);
await mkdir(sdkE2eRunDir, { recursive: true });

// Clean up old test runs, but keep the latest few for debugging
try {
const testRuns = await readdir(integrationTestsDir);
if (testRuns.length > 5) {
const oldRuns = testRuns.sort().slice(0, testRuns.length - 5);

// Clean up old CLI integration test runs (without sdk-e2e- prefix)
const cliTestRuns = testRuns.filter((run) => !run.startsWith('sdk-e2e-'));
if (cliTestRuns.length > 5) {
const oldRuns = cliTestRuns.sort().slice(0, cliTestRuns.length - 5);
await Promise.all(
oldRuns.map((oldRun) =>
rm(join(integrationTestsDir, oldRun), {
recursive: true,
force: true,
}),
),
);
}

// Clean up old SDK E2E test runs (with sdk-e2e- prefix)
const sdkTestRuns = testRuns.filter((run) => run.startsWith('sdk-e2e-'));
if (sdkTestRuns.length > 5) {
const oldRuns = sdkTestRuns.sort().slice(0, sdkTestRuns.length - 5);
await Promise.all(
oldRuns.map((oldRun) =>
rm(join(integrationTestsDir, oldRun), {
Expand All @@ -69,24 +92,37 @@ export async function setup() {
console.error('Error cleaning up old test runs:', e);
}

// Environment variables for CLI integration tests
process.env['INTEGRATION_TEST_FILE_DIR'] = runDir;
process.env['GEMINI_CLI_INTEGRATION_TEST'] = 'true';
process.env['TELEMETRY_LOG_FILE'] = join(runDir, 'telemetry.log');

// Environment variables for SDK E2E tests
process.env['E2E_TEST_FILE_DIR'] = sdkE2eRunDir;
process.env['TEST_CLI_PATH'] = join(rootDir, 'dist/cli.js');

if (process.env['KEEP_OUTPUT']) {
console.log(`Keeping output for test run in: ${runDir}`);
console.log(`Keeping output for SDK E2E test run in: ${sdkE2eRunDir}`);
}
process.env['VERBOSE'] = process.env['VERBOSE'] ?? 'false';

console.log(`\nIntegration test output directory: ${runDir}`);
console.log(`SDK E2E test output directory: ${sdkE2eRunDir}`);
console.log(`CLI path: ${process.env['TEST_CLI_PATH']}`);
}

export async function teardown() {
// Cleanup the test run directory unless KEEP_OUTPUT is set
// Cleanup the CLI test run directory unless KEEP_OUTPUT is set
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
await rm(runDir, { recursive: true, force: true });
}

// Cleanup the SDK E2E test run directory unless KEEP_OUTPUT is set
if (process.env['KEEP_OUTPUT'] !== 'true' && sdkE2eRunDir) {
await rm(sdkE2eRunDir, { recursive: true, force: true });
}

if (originalMemoryContent !== null) {
await mkdir(dirname(memoryFilePath), { recursive: true });
await writeFile(memoryFilePath, originalMemoryContent, 'utf-8');
Expand Down
Loading