From 328ffcec86daf0e367305570250f5e104d23914f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E9=9B=B2?= <137844255@qq.com> Date: Mon, 3 Aug 2026 09:55:30 +0800 Subject: [PATCH 1/3] fix(release): assign npm publishing to tag workflow --- packages/cli/release.config.js | 6 ++--- .../tests/unit/scripts/release-config.test.ts | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 packages/cli/tests/unit/scripts/release-config.test.ts diff --git a/packages/cli/release.config.js b/packages/cli/release.config.js index fb25e8a8..a9f1a873 100644 --- a/packages/cli/release.config.js +++ b/packages/cli/release.config.js @@ -52,8 +52,8 @@ export default { // 发布配置 publish: { - // 是否发布到 npm - npm: true, + // npm 发布由 tag workflow 统一负责,避免本地脚本与 CI 重复发布 + npm: false, // npm 发布配置 npmConfig: { access: 'public', @@ -94,4 +94,4 @@ export default { failure: '❌ 版本 {{version}} 发布失败:{{error}}', }, }, -}; \ No newline at end of file +}; diff --git a/packages/cli/tests/unit/scripts/release-config.test.ts b/packages/cli/tests/unit/scripts/release-config.test.ts new file mode 100644 index 00000000..19545774 --- /dev/null +++ b/packages/cli/tests/unit/scripts/release-config.test.ts @@ -0,0 +1,24 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +describe('release ownership contract', () => { + it('publishes npm only from the tag workflow', async () => { + const configPath = path.resolve(__dirname, '../../../release.config.js'); + const { default: releaseConfig } = (await import( + pathToFileURL(configPath).href + )) as { + default: { publish: { npm: boolean; git: boolean } }; + }; + const publishWorkflow = fs.readFileSync( + path.resolve(__dirname, '../../../../../.github/workflows/publish.yml'), + 'utf8' + ); + + expect(releaseConfig.publish.npm).toBe(false); + expect(releaseConfig.publish.git).toBe(true); + expect(publishWorkflow).toContain("- 'v*.*.*'"); + expect(publishWorkflow).toContain('npm publish --access public'); + }); +}); From 22d71f98e0944100379896ced91f74c0d24d35be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E9=9B=B2?= <137844255@qq.com> Date: Mon, 3 Aug 2026 09:56:40 +0800 Subject: [PATCH 2/3] fix(release): require webhook credentials from env --- packages/cli/release.config.js | 2 +- packages/cli/tests/unit/scripts/release-config.test.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/cli/release.config.js b/packages/cli/release.config.js index a9f1a873..4ec983e6 100644 --- a/packages/cli/release.config.js +++ b/packages/cli/release.config.js @@ -86,7 +86,7 @@ export default { methods: ['console', 'discord'], // Discord 配置 discord: { - webhookUrl: process.env.DISCORD_WEBHOOK_URL || 'https://discord.com/api/webhooks/1460226980938125387/5fWgMuGmkGtb6j3eoDaz4JtSFfH8LtFtHK9F2srIHGoXp71zm4sHFPCc729PujDbHJ2F', + webhookUrl: process.env.DISCORD_WEBHOOK_URL, }, // 通知模板 templates: { diff --git a/packages/cli/tests/unit/scripts/release-config.test.ts b/packages/cli/tests/unit/scripts/release-config.test.ts index 19545774..a50ee3d7 100644 --- a/packages/cli/tests/unit/scripts/release-config.test.ts +++ b/packages/cli/tests/unit/scripts/release-config.test.ts @@ -21,4 +21,13 @@ describe('release ownership contract', () => { expect(publishWorkflow).toContain("- 'v*.*.*'"); expect(publishWorkflow).toContain('npm publish --access public'); }); + + it('does not ship notification credentials in source', () => { + const configSource = fs.readFileSync( + path.resolve(__dirname, '../../../release.config.js'), + 'utf8' + ); + + expect(/https:\/\/discord\.com\/api\/webhooks\//.test(configSource)).toBe(false); + }); }); From 3d7445e9663f7a1963db7b216d816d8c3fbfc6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E9=9B=B2?= <137844255@qq.com> Date: Mon, 3 Aug 2026 10:12:01 +0800 Subject: [PATCH 3/3] test(release): strengthen publisher ownership contract --- .../tests/unit/scripts/release-config.test.ts | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/cli/tests/unit/scripts/release-config.test.ts b/packages/cli/tests/unit/scripts/release-config.test.ts index a50ee3d7..ff1201a9 100644 --- a/packages/cli/tests/unit/scripts/release-config.test.ts +++ b/packages/cli/tests/unit/scripts/release-config.test.ts @@ -3,31 +3,55 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { describe, expect, it } from 'vitest'; +const configPath = path.resolve(__dirname, '../../../release.config.js'); +const workflowsPath = path.resolve(__dirname, '../../../../../.github/workflows'); + +async function loadReleaseConfig(cacheKey: string) { + return (await import(`${pathToFileURL(configPath).href}?${cacheKey}`)).default as { + publish: { npm: boolean; git: boolean }; + notifications: { discord: { webhookUrl?: string } }; + }; +} + describe('release ownership contract', () => { it('publishes npm only from the tag workflow', async () => { - const configPath = path.resolve(__dirname, '../../../release.config.js'); - const { default: releaseConfig } = (await import( - pathToFileURL(configPath).href - )) as { - default: { publish: { npm: boolean; git: boolean } }; - }; + const releaseConfig = await loadReleaseConfig('publish-owner'); + const workflowFiles = fs + .readdirSync(workflowsPath) + .filter((file) => /\.ya?ml$/.test(file)); + const npmPublishers = workflowFiles.filter((file) => + fs.readFileSync(path.join(workflowsPath, file), 'utf8').includes('npm publish') + ); const publishWorkflow = fs.readFileSync( - path.resolve(__dirname, '../../../../../.github/workflows/publish.yml'), + path.join(workflowsPath, 'publish.yml'), 'utf8' ); expect(releaseConfig.publish.npm).toBe(false); expect(releaseConfig.publish.git).toBe(true); + expect(npmPublishers).toEqual(['publish.yml']); expect(publishWorkflow).toContain("- 'v*.*.*'"); expect(publishWorkflow).toContain('npm publish --access public'); }); - it('does not ship notification credentials in source', () => { - const configSource = fs.readFileSync( - path.resolve(__dirname, '../../../release.config.js'), - 'utf8' - ); + it('loads notification credentials only from the environment', async () => { + const previousWebhook = process.env.DISCORD_WEBHOOK_URL; + process.env.DISCORD_WEBHOOK_URL = 'https://example.invalid/test-webhook'; + + try { + const releaseConfig = await loadReleaseConfig('notification-env'); + const configSource = fs.readFileSync(configPath, 'utf8'); - expect(/https:\/\/discord\.com\/api\/webhooks\//.test(configSource)).toBe(false); + expect(releaseConfig.notifications.discord.webhookUrl).toBe( + 'https://example.invalid/test-webhook' + ); + expect(/https:\/\/discord\.com\/api\/webhooks\//.test(configSource)).toBe(false); + } finally { + if (previousWebhook === undefined) { + delete process.env.DISCORD_WEBHOOK_URL; + } else { + process.env.DISCORD_WEBHOOK_URL = previousWebhook; + } + } }); });