feat(web-shell): publish the package with the cli release (#12178)

* 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 '<dir>' 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 <[email protected]>
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 <[email protected]>
This commit is contained in:
易良andQwen-Coder authored and GitHub committed 2026-09-18 12:17:07 +00:00
1 parent ab47e38197
commit e596a410bb
6 files changed
+46 -7

No files matched your search

+6
View File
@@ -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
+5
View File
@@ -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",
+1
View File
@@ -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:
+8 -6
View File
@@ -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',
]);
});
+1
View File
@@ -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'),
+25 -1
View File
@@ -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');