From e596a410bbd58313b6db069eed1cbb33bd3dbd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Fri, 18 Sep 2026 12:17:07 +0000 Subject: [PATCH] feat(web-shell): publish the package with the cli release (#12178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(web-shell): publish the package with the cli release The release workflow published the cli and the channel packages but never @qwen-code/web-shell, so the npm artifact only moved when someone published it by hand. Add the package to the release step and record its repository so npm accepts the provenance attestation the workflow requests. * fix(release): probe @qwen-code/web-shell in the push-time version guard publish_package 'packages/web-shell' made the package CI-published, but PUBLISHED_PACKAGES — the sole input to assertVersionUnreleased — still listed only twelve names. A release shipped only as @qwen-code/web-shell therefore read as unshipped: the version-collision bump never fired and the force-push guard could proceed over an already-published version. Widen the guard list, its pin in get-release-version.test.js, and generalise "keeps the publish allowlist and the guard package set in step" so every bare publish_package '' target is resolved through the package.json it publishes. The channel-loop comparison alone could not see this addition, which is how it shipped unguarded. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmu6tb0j73c * test(release): simplify publish guard coverage * fix(release): publish web-shell after the channels Review on #12178 (R1-3): the step runs under `set -eo pipefail` and every publish_package call is bare, so inserting the newest name ahead of the channel packages makes a failure of its first CI publish strand all of them. Publish it last instead. Also append the new entry to PUBLISHED_PACKAGES rather than inserting it after audio-capture: the version-guard tests read that array by index, so its position is load-bearing. Record that in the pinned assertion. --------- Co-authored-by: Qwen-Coder --- .github/scripts/run-release-step.sh | 6 ++++++ packages/web-shell/package.json | 5 +++++ scripts/assert-release-version.mjs | 1 + scripts/tests/get-release-version.test.js | 14 ++++++------ scripts/tests/package-scripts.test.js | 1 + scripts/tests/release-workflow.test.js | 26 ++++++++++++++++++++++- 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.github/scripts/run-release-step.sh b/.github/scripts/run-release-step.sh index 8be120125dd..79d6beb903a 100755 --- a/.github/scripts/run-release-step.sh +++ b/.github/scripts/run-release-step.sh @@ -197,6 +197,12 @@ case "${step}" in publish_package "packages/channels/${channel}" "${publish_marker}" "${channel}" echo "::endgroup::" done + + # Last on purpose: this is the only name in the sequence CI has never + # published, so a failure here leaves every long-shipped package already + # out instead of stranding them under `set -eo pipefail`. + publish_package 'packages/web-shell' + if [[ "${IS_DRY_RUN}" != "true" ]] && [[ ! -s "${publish_marker}" ]]; then echo "::warning::Every channel package was already published; nothing shipped" fi diff --git a/packages/web-shell/package.json b/packages/web-shell/package.json index 071e29d8c78..7f195845536 100644 --- a/packages/web-shell/package.json +++ b/packages/web-shell/package.json @@ -1,6 +1,11 @@ { "name": "@qwen-code/web-shell", "version": "0.24.0", + "repository": { + "type": "git", + "url": "git+https://github.com/QwenLM/qwen-code.git", + "directory": "packages/web-shell" + }, "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/scripts/assert-release-version.mjs b/scripts/assert-release-version.mjs index c6280484aa7..8b3ab47cd50 100644 --- a/scripts/assert-release-version.mjs +++ b/scripts/assert-release-version.mjs @@ -22,6 +22,7 @@ export const PUBLISHED_PACKAGES = [ '@qwen-code/channel-telegram', '@qwen-code/channel-wecom', '@qwen-code/channel-weixin', + '@qwen-code/web-shell', ]; // Deliberately a copy of `scripts/lib/release-helpers.js`, not an import: diff --git a/scripts/tests/get-release-version.test.js b/scripts/tests/get-release-version.test.js index 995e99a0549..97ca4cbe56b 100644 --- a/scripts/tests/get-release-version.test.js +++ b/scripts/tests/get-release-version.test.js @@ -738,12 +738,13 @@ describe('assertVersionUnreleased', () => { `Version 1.2.3 has already shipped; refusing to force-push the release branch over it. Found on: ${foundOn}. If a previous attempt published only part of the release, complete the remaining artifacts manually — re-running this job will keep failing here while the version stays published.`; it('pins the full published-package set', () => { - // The push-time guard derives from this list; the publish allowlist in - // `.github/scripts/run-release-step.sh` enumerates the same channels - // separately, so adding or removing a package must update both. The two - // are pinned against each other by "keeps the publish allowlist and the - // guard package set in step" in release-workflow.test.js — release.yml - // itself no longer contains any publish step. + // The push-time guard and the release-version picker derive from this + // list; the publish allowlist in `.github/scripts/run-release-step.sh` + // enumerates the same packages separately, so adding or removing one must + // update both. They are pinned against each other by "keeps the publish + // allowlist and the guard package set in step" in release-workflow.test.js + // — release.yml itself no longer contains any publish step. Append rather + // than insert: the tests below read this array by index. expect(PUBLISHED_PACKAGES).toEqual([ '@qwen-code/qwen-code', '@qwen-code/external-context-mem0', @@ -757,6 +758,7 @@ describe('assertVersionUnreleased', () => { '@qwen-code/channel-telegram', '@qwen-code/channel-wecom', '@qwen-code/channel-weixin', + '@qwen-code/web-shell', ]); }); diff --git a/scripts/tests/package-scripts.test.js b/scripts/tests/package-scripts.test.js index 709be2839ed..a936a28fb13 100644 --- a/scripts/tests/package-scripts.test.js +++ b/scripts/tests/package-scripts.test.js @@ -1772,6 +1772,7 @@ describe('package scripts', () => { 'packages/mobile-mcp', 'packages/node-repl', 'packages/sdk-typescript', + 'packages/web-shell', ]) { const packageJson = JSON.parse( readFileSync(path.join(root, packageDirectory, 'package.json'), 'utf8'), diff --git a/scripts/tests/release-workflow.test.js b/scripts/tests/release-workflow.test.js index d2c4e93fe77..b9f50052a67 100644 --- a/scripts/tests/release-workflow.test.js +++ b/scripts/tests/release-workflow.test.js @@ -524,6 +524,28 @@ describe('release workflow', () => { .filter((name) => name !== 'base') .sort(); expect(published).toEqual(guarded); + + // Non-channel packages are literal calls, so verify those against the + // guard too. Resolve their names from package.json to avoid a second map. + const publishStep = releaseStepScript.slice( + releaseStepScript.indexOf('\n publish-packages)'), + releaseStepScript.indexOf('\n verify-archives)'), + ); + const literalTargets = [ + ...publishStep.matchAll(/publish_package '([^']+)'/g), + ].map(([, directory]) => directory); + expect(literalTargets.length).toBeGreaterThan(1); + const guardedNames = new Set(PUBLISHED_PACKAGES); + for (const directory of literalTargets) { + // `dist` is the root package's bundle output rather than a workspace + // directory, and does not exist in a source checkout. + const manifest = + directory === 'dist' ? 'package.json' : `${directory}/package.json`; + const { name } = JSON.parse(readFileSync(manifest, 'utf8')); + expect(guardedNames.has(name), `${directory} publishes ${name}`).toBe( + true, + ); + } }); it('keeps the workflow focused on orchestration', () => { @@ -598,6 +620,7 @@ describe('release workflow', () => { mkdirSync(bin); for (const path of [ 'dist', + 'packages/web-shell', 'packages/channels/base', ...channels.map((channel) => `packages/channels/${channel}`), ]) { @@ -643,12 +666,13 @@ describe('release workflow', () => { expect(publishCalls.map(([cwd]) => cwd)).toEqual([ join(canonicalDirectory, 'dist'), join(canonicalDirectory, 'packages/channels/base'), + join(canonicalDirectory, 'packages/web-shell'), ]); // The dist-tag is the reason NPM_TAG is set in this child env at all. // Without it `npm publish` defaults to `latest`, so the 21:00 UTC // nightly would take over the tag every end-user install and the ECS // fleet updater resolve through. `--access public` is here for the - // same reason: one array feeds all twelve published packages. + // same reason: one array feeds all thirteen published packages. for (const [cwd, args] of publishCalls) { expect(args, cwd).toContain('--access public'); expect(args, cwd).toContain('--tag=latest');