diff --git a/.github/workflows/.size-baseline b/.github/workflows/.size-baseline index 97889894aab..c57c5d4abae 100644 --- a/.github/workflows/.size-baseline +++ b/.github/workflows/.size-baseline @@ -47,7 +47,7 @@ 22037 release-sdk-python.yml 19094 release-sdk.yml 14546 release-vscode-companion.yml -59855 release.yml +62799 release.yml 43717 repo-hygiene.yml 1079 scorecard-monthly.yml 10691 sdk-java.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33d50168ebd..e46f52cf5b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -839,7 +839,7 @@ jobs: RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' CI_BOT_PAT: '${{ secrets.CI_BOT_PAT }}' run: |- - git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json + git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json integrations/*/qwen-extension.json if git diff --staged --quiet; then echo "No version changes to commit" else @@ -942,6 +942,28 @@ jobs: QWEN_STANDALONE_REQUIRE_AUDIO_CAPTURE_PREBUILD: "${{ github.repository == 'QwenLM/qwen-code' && '1' || '' }}" run: 'npm run package:standalone:release -- --version "${RELEASE_VERSION}" --out-dir dist/standalone' + # npm trusted publishing can only be configured after a package exists. + # Keep this disabled until the one-time bootstrap publish and trusted + # publisher setup are complete. + - name: 'Publish @qwen-code/external-context-mem0' + if: |- + ${{ github.repository == 'QwenLM/qwen-code' && vars.NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED == 'true' }} + working-directory: 'integrations/external-context-mem0' + run: |- + PACKAGE_NAME="$(node -p "require('./package.json').name")" + PUBLISH_ARGS=(--access public "--tag=${NPM_TAG}") + if [[ "${IS_DRY_RUN}" == "true" ]]; then + PUBLISH_ARGS+=(--dry-run) + elif npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version >/dev/null 2>&1; then + echo "::notice::${PACKAGE_NAME}@${RELEASE_VERSION} already published; skipping" + exit 0 + fi + npm publish --provenance "${PUBLISH_ARGS[@]}" + env: + RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' + NPM_TAG: '${{ needs.prepare.outputs.npm_tag }}' + IS_DRY_RUN: '${{ needs.prepare.outputs.is_dry_run }}' + - name: 'Publish @qwen-code/audio-capture' if: |- ${{ github.repository == 'QwenLM/qwen-code' }} diff --git a/docs/design/external-context-mem0-extension.md b/docs/design/external-context-mem0-extension.md index 1d568858197..4e6e47d0231 100644 --- a/docs/design/external-context-mem0-extension.md +++ b/docs/design/external-context-mem0-extension.md @@ -228,6 +228,26 @@ The npm package publishes only the bundled runtime, canonical schemas, Extension manifest, and README. It contains no administrator dialect, provider preset, provider identifier, or provider-specific contract fixture. +The public package name is `@qwen-code/external-context-mem0`. Its package and +Extension manifest versions follow the Qwen Code release version and are +updated in the same release commit. Administrators can install the latest +release with `qwen extensions install @qwen-code/external-context-mem0` or pin +an explicit npm version. Installation never creates an instance file, dialect +file, credential, or ordinary Qwen setting. + +The normal release workflow builds the self-contained bundle, checks whether +the exact package version already exists, and publishes with the release's npm +dist-tag and provenance. The package participates in the shared +already-published guard so a partial release cannot be overwritten by a retry. +Because npm requires a package to exist before trusted publishing can be +configured, the publish step remains behind the +`NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED` repository variable +until a maintainer completes the one-time public bootstrap publish and binds +the package to the `release.yml` workflow in the `production-release` +environment. The bootstrap should publish the first actual Qwen release that +contains this change, not invent a second version line. Future releases use +trusted publishing and require no npm token. + When a service fits `DialectV1`, its administrator writes and validates a local dialect file. When it does not fit, the service owner publishes a separate MCP Extension implementing External Context MCP Profile v1. Qwen does not add a @@ -243,7 +263,9 @@ built-in provider rollout for either case. 3. **PR2:** Replace the unused registry with administrator-owned `InstanceConfigV2` and `DialectV1` files, while preserving the request engine and profile boundary. -4. Design any portable write capability separately. +4. **Distribution follow-up:** Publish the self-contained Extension through the + normal Qwen Code npm release without adding provider data or Core wiring. +5. Design any portable write capability separately. There is no Qwen-maintained provider-preset PR3. Administrators own compatible dialect data; incompatible protocols use their own MCP Extension. diff --git a/integrations/external-context-mem0/README.md b/integrations/external-context-mem0/README.md index cb6bce589d4..0780dd52028 100644 --- a/integrations/external-context-mem0/README.md +++ b/integrations/external-context-mem0/README.md @@ -5,6 +5,27 @@ for administrator-configured Mem0-compatible HTTP services. It validates a closed dialect grammar and uses a bounded HTTP request engine; it does not ship provider presets or provider-specific configuration. +## Installation + +Install the published Extension with Qwen Code: + +```bash +qwen extensions install @qwen-code/external-context-mem0 +``` + +This command becomes available after the package's first registry release. + +Use an explicit package version when the deployment must remain pinned: + +```bash +qwen extensions install @qwen-code/external-context-mem0@x.y.z +``` + +The Extension version follows the Qwen Code release version. Installing the +package does not configure a memory service or install provider data; an +administrator must supply the two files and credential environment described +below. + ## Configuration Set `QWEN_EXTERNAL_CONTEXT_MEM0_CONFIG` in the Extension process environment to diff --git a/integrations/external-context-mem0/package.json b/integrations/external-context-mem0/package.json index 9b293069d7f..3441d660987 100644 --- a/integrations/external-context-mem0/package.json +++ b/integrations/external-context-mem0/package.json @@ -1,8 +1,12 @@ { "name": "@qwen-code/external-context-mem0", "version": "0.22.3", - "private": true, "description": "Configurable Mem0-compatible External Context Extension", + "repository": { + "type": "git", + "url": "git+https://github.com/QwenLM/qwen-code.git", + "directory": "integrations/external-context-mem0" + }, "type": "module", "engines": { "node": ">=22.0.0" diff --git a/integrations/external-context-mem0/qwen-extension.json b/integrations/external-context-mem0/qwen-extension.json index ea7f5d99621..5e80f5dcf62 100644 --- a/integrations/external-context-mem0/qwen-extension.json +++ b/integrations/external-context-mem0/qwen-extension.json @@ -8,7 +8,7 @@ "en": "Configurable retrieval from an administrator-bound Mem0-compatible service", "zh": "从管理员绑定的 Mem0 兼容服务检索外部上下文" }, - "version": "0.1.0", + "version": "0.22.3", "mcpServers": { "external-context-mem0": { "command": "node", diff --git a/integrations/external-context-mem0/src/manifest.test.ts b/integrations/external-context-mem0/src/manifest.test.ts index f43befdb591..20ef8086b3d 100644 --- a/integrations/external-context-mem0/src/manifest.test.ts +++ b/integrations/external-context-mem0/src/manifest.test.ts @@ -28,6 +28,14 @@ describe('Mem0 Extension package', () => { expect(packageJson.scripts?.['build']).toContain('--bundle'); expect(packageJson.files).toContain('dist/main.js'); expect(packageJson.dependencies).toBeUndefined(); + expect(packageJson.private).not.toBe(true); + expect(packageJson.name).toBe('@qwen-code/external-context-mem0'); + expect(packageJson.version).toBe(manifest.version); + expect(packageJson.repository).toEqual({ + type: 'git', + url: 'git+https://github.com/QwenLM/qwen-code.git', + directory: 'integrations/external-context-mem0', + }); }); it('ships only the runtime, schemas, manifest, and documentation', async () => { @@ -43,11 +51,20 @@ describe('Mem0 Extension package', () => { }); interface Manifest { + version?: string; mcpServers?: Record>; settings?: unknown; } interface PackageJson { + name?: string; + version?: string; + private?: boolean; + repository?: { + type?: string; + url?: string; + directory?: string; + }; dependencies?: Record; scripts?: Record; files?: string[]; diff --git a/scripts/get-release-version.js b/scripts/get-release-version.js index bee2c591ff7..7d0741ed191 100644 --- a/scripts/get-release-version.js +++ b/scripts/get-release-version.js @@ -182,6 +182,7 @@ function detectRollbackAndGetBaseline(npmDistTag) { */ export const PUBLISHED_PACKAGES = [ '@qwen-code/qwen-code', + '@qwen-code/external-context-mem0', '@qwen-code/audio-capture', '@qwen-code/channel-base', '@qwen-code/channel-dingtalk', diff --git a/scripts/tests/get-release-version.test.js b/scripts/tests/get-release-version.test.js index f4da22c6302..76abdc9b4ca 100644 --- a/scripts/tests/get-release-version.test.js +++ b/scripts/tests/get-release-version.test.js @@ -738,6 +738,7 @@ describe('assertVersionUnreleased', () => { // release.yml so every consumer is reviewed together. expect(PUBLISHED_PACKAGES).toEqual([ '@qwen-code/qwen-code', + '@qwen-code/external-context-mem0', '@qwen-code/audio-capture', '@qwen-code/channel-base', '@qwen-code/channel-dingtalk', diff --git a/scripts/tests/package-scripts.test.js b/scripts/tests/package-scripts.test.js index e628bed1b10..4c6959d8799 100644 --- a/scripts/tests/package-scripts.test.js +++ b/scripts/tests/package-scripts.test.js @@ -65,6 +65,27 @@ describe('package scripts', () => { expect(buildOrder).toContain("'packages/qwen-live',"); }); + it('keeps the Mem0 Extension manifest aligned with release versions', () => { + const versionScript = readFileSync( + path.join(root, 'scripts/version.js'), + 'utf8', + ); + + expect(versionScript).toContain( + "'integrations/external-context-mem0/qwen-extension.json'", + ); + expect(versionScript).toContain( + 'const mem0Manifest = readJson(mem0ManifestPath);', + ); + expect(versionScript).toContain('mem0Manifest.version = newVersion'); + expect(versionScript).toContain( + 'writeJson(mem0ManifestPath, mem0Manifest);', + ); + expect(versionScript).toContain( + "'npx prettier --experimental-cli --write integrations/external-context-mem0/qwen-extension.json'", + ); + }); + it('keeps the serve fast-path bundle check outside unit test scripts', () => { const packageJson = readPackageJson(); @@ -515,6 +536,7 @@ describe('package scripts', () => { const publishJob = getWorkflowJob(workflow, 'publish'); for (const stepName of [ + 'Publish @qwen-code/external-context-mem0', 'Publish @qwen-code/audio-capture', 'Publish @qwen-code/qwen-code', 'Publish @qwen-code/channel-base', @@ -554,6 +576,11 @@ describe('package scripts', () => { it('meets npm trusted publishing requirements', () => { for (const [workflowPath, jobName, publishStepName] of [ + [ + '.github/workflows/release.yml', + 'publish', + 'Publish @qwen-code/external-context-mem0', + ], [ '.github/workflows/release.yml', 'publish', @@ -589,6 +616,7 @@ describe('package scripts', () => { } for (const packageDirectory of [ + 'integrations/external-context-mem0', 'packages/audio-capture', 'packages/cli', 'packages/channels/base', diff --git a/scripts/tests/release-workflow.test.js b/scripts/tests/release-workflow.test.js index 41c913c5a77..afceafa063b 100644 --- a/scripts/tests/release-workflow.test.js +++ b/scripts/tests/release-workflow.test.js @@ -1305,10 +1305,28 @@ describe('release workflow', () => { it('stages every integration package manifest after versioning', () => { expect(workflow).toContain( - 'git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json', + 'git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json integrations/*/qwen-extension.json', ); }); + it('publishes the Mem0 Extension only after trusted publishing bootstrap', () => { + const publishSteps = releaseYaml.jobs.publish.steps; + const mem0Step = publishSteps.find( + (step) => step.name === 'Publish @qwen-code/external-context-mem0', + ); + const audioStepIndex = publishSteps.findIndex( + (step) => step.name === 'Publish @qwen-code/audio-capture', + ); + + expect(mem0Step.if).toContain( + "vars.NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED == 'true'", + ); + expect(mem0Step['working-directory']).toBe( + 'integrations/external-context-mem0', + ); + expect(publishSteps.indexOf(mem0Step)).toBeLessThan(audioStepIndex); + }); + it('fires the fleet-moving npm-published dispatch on stable releases only', () => { // This gate is the sole protection keeping a nightly/preview/dry-run // release from moving the ECS fleet; the triggered update workflow diff --git a/scripts/version.js b/scripts/version.js index 598cf9fad8f..c6a56c0b8f4 100644 --- a/scripts/version.js +++ b/scripts/version.js @@ -69,7 +69,19 @@ for (const workspaceName of workspacesToVersion) { const rootPackageJsonPath = resolve(process.cwd(), 'package.json'); const newVersion = readJson(rootPackageJsonPath).version; -// 5. Update the sandboxImageUri in the root package.json +// 5. Keep the published Mem0 Extension manifest aligned with its package. +const mem0ManifestPath = resolve( + process.cwd(), + 'integrations/external-context-mem0/qwen-extension.json', +); +const mem0Manifest = readJson(mem0ManifestPath); +mem0Manifest.version = newVersion; +writeJson(mem0ManifestPath, mem0Manifest); +run( + 'npx prettier --experimental-cli --write integrations/external-context-mem0/qwen-extension.json', +); + +// 6. Update the sandboxImageUri in the root package.json const rootPackageJson = readJson(rootPackageJsonPath); if (rootPackageJson.config?.sandboxImageUri) { rootPackageJson.config.sandboxImageUri = @@ -78,7 +90,7 @@ if (rootPackageJson.config?.sandboxImageUri) { writeJson(rootPackageJsonPath, rootPackageJson); } -// 6. Update the sandboxImageUri in the cli package.json +// 7. Update the sandboxImageUri in the cli package.json const cliPackageJsonPath = resolve(process.cwd(), 'packages/cli/package.json'); const cliPackageJson = readJson(cliPackageJsonPath); if (cliPackageJson.config?.sandboxImageUri) { @@ -90,7 +102,7 @@ if (cliPackageJson.config?.sandboxImageUri) { writeJson(cliPackageJsonPath, cliPackageJson); } -// 7. Pin channel adapters' semver dependency on @qwen-code/channel-base to +// 8. Pin channel adapters' semver dependency on @qwen-code/channel-base to // the exact new version. A caret range like ^0.21.0 does not match a // prerelease bump (e.g. 0.21.1-preview.0), so npm would replace the workspace // link with the stale registry package and the release build would compile @@ -110,14 +122,14 @@ for (const entry of readdirSync(channelsDir)) { } } -// 8. Refresh node_modules and package-lock.json against the pinned exact +// 9. Refresh node_modules and package-lock.json against the pinned exact // versions so the adapters resolve channel-base to the workspace link again. // --ignore-scripts prevents the root `prepare` lifecycle from triggering a // redundant full build that fails with TS5055 when dist/ already exists from // the initial `npm ci` install. run('npm install --ignore-scripts'); -// 9. The per-workspace `npm version` reifies above nested a stale registry +// 10. The per-workspace `npm version` reifies above nested a stale registry // copy of channel-base under each adapter while ranges briefly mismatched. // The install above cleans both lockfiles but can leave that directory on // disk, where it shadows the workspace link during tsc. Remove it.