diff --git a/packages/cli/release.config.js b/packages/cli/release.config.js index fb25e8a8..4ec983e6 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', @@ -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: { @@ -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..ff1201a9 --- /dev/null +++ b/packages/cli/tests/unit/scripts/release-config.test.ts @@ -0,0 +1,57 @@ +import fs from 'node:fs'; +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 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.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('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(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; + } + } + }); +});