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');