diff --git a/docs/maintainer/release-workflow.md b/docs/maintainer/release-workflow.md index 9fe63bf2..6d6bb6d5 100644 --- a/docs/maintainer/release-workflow.md +++ b/docs/maintainer/release-workflow.md @@ -56,10 +56,15 @@ Required release assets: - `styles.css` - `README.md` -## 5. Suggested Release Command Pattern +## 5. Publish Command ```bash -gh release create main.js manifest.json styles.css README.md \ - --title "Notemd " \ - --notes-file docs/releases/.md +npm run release:github -- ``` + +The helper enforces the required packaged assets and `docs/releases/.md` before invoking GitHub: + +- If the release does not exist yet, it runs `gh release create ... --verify-tag`. +- If the release already exists, it runs `gh release upload ... --clobber`. + +That second path is the repair path for cases where a release body was published but plugin assets were not uploaded. diff --git a/docs/maintainer/release-workflow.zh-CN.md b/docs/maintainer/release-workflow.zh-CN.md index 63487531..83aab7b8 100644 --- a/docs/maintainer/release-workflow.zh-CN.md +++ b/docs/maintainer/release-workflow.zh-CN.md @@ -56,10 +56,15 @@ Release 必需资产: - `styles.css` - `README.md` -## 5. 推荐发布命令模板 +## 5. 发布命令 ```bash -gh release create main.js manifest.json styles.css README.md \ - --title "Notemd " \ - --notes-file docs/releases/.md +npm run release:github -- ``` + +该辅助命令会在调用 GitHub 前强制校验必须打包的资产以及 `docs/releases/.md`: + +- 如果 release 尚不存在,则执行 `gh release create ... --verify-tag`。 +- 如果 release 已存在,则执行 `gh release upload ... --clobber`。 + +第二条路径就是这类问题的修复路径:release 文案已发布,但插件安装资产未上传时,可直接补传。 diff --git a/docs/releases/1.8.2.md b/docs/releases/1.8.2.md new file mode 100644 index 00000000..e8ae4f85 --- /dev/null +++ b/docs/releases/1.8.2.md @@ -0,0 +1,57 @@ +## English + +### 💡 Highlights +This release finishes the language-support rollout that 1.8.0 and 1.8.1 started. Frontend labels now follow Obsidian's language selection across the shipped UI locale catalog much more consistently, including advanced settings, provider/task-language controls, diagnostics, concept-note settings, and quick-workflow surfaces that previously could still leak English after update. + +It also brings the documentation back into line with the code. The remaining translated README variants were aligned against the original English README in one pass, including language-support architecture, configuration details, and troubleshooting sections. Release safety is tighter too: Notemd now gates locale coverage, translated README English leakage, hardcoded UI strings, and tooling isolation around `ref/**` before shipping. + +### 🚨 Breaking Changes +* **No user-facing migration is required**: Existing installations keep their current behavior. `uiLocale: auto` still follows Obsidian, and existing task-language settings continue to work without manual reconfiguration. + +### ✨ New Features +* `feat`: Expanded shipped UI locale coverage for frontend settings/sidebar/runtime copy so supported Obsidian locales resolve more surfaces without falling back to English. +* `feat`: Added audit coverage for translated README English leakage and tooling-isolation checks around `ref/**`. +* `feat`: Added/extended locale coverage tests for provider configuration, remaining settings surfaces, and i18n audit script configuration. + +### 🐛 Bug Fixes +* `fix`: Fixed the 1.8.0/1.8.1 regression where parts of the frontend could still show English after upgrade even when Obsidian UI language had changed. +* `fix`: Closed remaining untranslated labels in advanced language settings, retry/diagnostic controls, concept-note output settings, translation settings, and quick-workflow UI surfaces. +* `fix`: Aligned the remaining published README translations with the original English README, including troubleshooting and language-support sections. + +### 🛠️ Refactors & Chores +* `refactor`: Consolidated additional locale strings into shared locale-extension catalogs and kept the core locale catalogs synchronized. +* `chore`: Hardened TypeScript/Jest/audit isolation so the local reference repository under `ref/**` does not pollute normal build and test scopes. +* `docs`: Updated README versioning and multilingual language-support docs to match the current shipped implementation. + +### 🙏 Contributors +Thanks to `@Jacobinwwey` for driving the locale-coverage completion, README alignment, and verification-gate hardening for this release. + +--- + +## 中文 + +### 💡 核心亮点 +本次版本补完了 1.8.0 与 1.8.1 启动的语言支持收口工作。前端设置项、侧边栏、任务/提供商语言配置、诊断区、概念笔记设置以及快速工作流等界面文案,现在会更稳定地跟随 Obsidian 当前语言,而不是在升级后仍局部残留英文。 + +同时,这一版把文档状态重新拉齐到了代码现状。此前尚未完全对齐的多语 README,现已按英文原始 README 一次性补齐,包括语言架构、配置细节与故障排查章节。发布门禁也进一步收紧:现在会在发布前检查 locale 覆盖、翻译版 README 英文泄漏、硬编码 UI 文案以及 `ref/**` 工具链隔离,避免语言支持继续以不完整状态进入版本发布。 + +### 🚨 破坏性更新 +* **无需用户手动迁移**:现有安装升级后继续沿用当前行为。`uiLocale: auto` 仍然跟随 Obsidian,既有任务输出语言配置也无需手动调整。 + +### ✨ 新特性 +* `feat`: 扩展前端 UI locale 覆盖,使已支持的 Obsidian 语言在设置页、侧边栏与关键运行时文案上更少回退到英文。 +* `feat`: 新增翻译版 README 英文泄漏审计,以及围绕 `ref/**` 的工具链隔离校验。 +* `feat`: 新增/补强 provider 配置、剩余设置表面和 i18n 审计脚本配置的 locale 覆盖测试。 + +### 🐛 Bug 修复 +* `fix`: 修复 1.8.0 / 1.8.1 中升级后前端部分文案仍可能停留在英文、未随 Obsidian 语言切换的问题。 +* `fix`: 补齐高级语言设置、重试与诊断控制、概念笔记输出设置、翻译设置、快速工作流等区域残留的未翻译标签。 +* `fix`: 将剩余多语 README 与英文原始 README 完整对齐,补齐语言支持与故障排查等章节内容。 + +### 🛠️ 优化与重构 +* `refactor`: 将新增 locale 文案收敛到共享 locale 扩展目录,并保持核心语言目录同步。 +* `chore`: 强化 TypeScript / Jest / 审计脚本对 `ref/**` 的隔离,避免本地参考仓污染正常构建与测试范围。 +* `docs`: 更新 README 版本号与多语语言支持文档,使其与当前实际发货实现一致。 + +### 🙏 致谢 +感谢 `@Jacobinwwey` 推进本次语言覆盖补齐、README 对齐与发布验证门禁强化。 diff --git a/package.json b/package.json index f0b77c2f..e2776bcb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "eslint . --ext .ts", "diagnose:llm": "node scripts/diagnose-llm-provider.js", "audit:i18n-ui": "node scripts/audit-hardcoded-ui-strings.js", + "release:github": "node scripts/release/publish-github-release.js", "regression:language-baseline": "bash scripts/regression/language-support-baseline.sh", "regression:language-compare": "bash scripts/regression/language-support-compare.sh" }, diff --git a/scripts/release/publish-github-release.js b/scripts/release/publish-github-release.js new file mode 100644 index 00000000..157f9296 --- /dev/null +++ b/scripts/release/publish-github-release.js @@ -0,0 +1,102 @@ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const REQUIRED_RELEASE_ASSETS = ['main.js', 'manifest.json', 'styles.css', 'README.md']; + +function ensureFileExists(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error(`Missing required release file: ${filePath}`); + } +} + +function resolveReleaseInputs(repoRoot, tag) { + if (!tag) { + throw new Error('Usage: node scripts/release/publish-github-release.js [--dry-run]'); + } + + const assets = REQUIRED_RELEASE_ASSETS.map((assetName) => path.join(repoRoot, assetName)); + assets.forEach(ensureFileExists); + + const notesFile = path.join(repoRoot, 'docs', 'releases', `${tag}.md`); + ensureFileExists(notesFile); + + return { + tag, + title: `Notemd ${tag}`, + notesFile, + assets + }; +} + +function buildGhReleaseCommand({ tag, title, notesFile, assets, releaseExists }) { + if (releaseExists) { + return ['release', 'upload', tag, ...assets, '--clobber']; + } + + return ['release', 'create', tag, ...assets, '--title', title, '--notes-file', notesFile, '--verify-tag']; +} + +function hasExistingRelease(repoRoot, tag) { + const result = spawnSync('gh', ['release', 'view', tag], { + cwd: repoRoot, + stdio: 'ignore' + }); + + if (result.error) { + throw result.error; + } + + return result.status === 0; +} + +function runGhCommand(repoRoot, commandArgs) { + const result = spawnSync('gh', commandArgs, { + cwd: repoRoot, + stdio: 'inherit' + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`gh ${commandArgs.join(' ')} failed with exit code ${result.status}`); + } +} + +function main(argv = process.argv.slice(2)) { + const dryRun = argv.includes('--dry-run'); + const tag = argv.find((arg) => !arg.startsWith('--')); + const repoRoot = path.join(__dirname, '..', '..'); + const inputs = resolveReleaseInputs(repoRoot, tag); + const commandArgs = buildGhReleaseCommand({ + ...inputs, + releaseExists: hasExistingRelease(repoRoot, tag) + }); + + if (dryRun) { + console.log(`gh ${commandArgs.join(' ')}`); + return 0; + } + + runGhCommand(repoRoot, commandArgs); + return 0; +} + +if (require.main === module) { + try { + process.exitCode = main(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} + +module.exports = { + REQUIRED_RELEASE_ASSETS, + buildGhReleaseCommand, + hasExistingRelease, + main, + resolveReleaseInputs +}; diff --git a/src/tests/githubReleaseWorkflow.test.ts b/src/tests/githubReleaseWorkflow.test.ts new file mode 100644 index 00000000..0ab4c24f --- /dev/null +++ b/src/tests/githubReleaseWorkflow.test.ts @@ -0,0 +1,133 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +describe('GitHub release workflow', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const packageJsonPath = path.join(repoRoot, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const releaseScriptRelativePath = path.join('scripts', 'release', 'publish-github-release.js'); + const releaseScriptPath = path.join(repoRoot, releaseScriptRelativePath); + + test('exposes a checked-in GitHub release helper and notes for the current version', () => { + expect(packageJson.scripts['release:github']).toBe(`node ${releaseScriptRelativePath}`); + expect(fs.existsSync(releaseScriptPath)).toBe(true); + + const currentReleaseNotesPath = path.join(repoRoot, 'docs', 'releases', `${packageJson.version}.md`); + expect(fs.existsSync(currentReleaseNotesPath)).toBe(true); + }); + + const maybeDescribeReleaseScript = fs.existsSync(releaseScriptPath) ? describe : describe.skip; + + maybeDescribeReleaseScript('publish-github-release helper', () => { + let buildGhReleaseCommand: (args: { + tag: string; + title: string; + notesFile: string; + assets: string[]; + releaseExists: boolean; + }) => string[]; + let resolveReleaseInputs: (repoRoot: string, tag: string) => { + tag: string; + title: string; + notesFile: string; + assets: string[]; + }; + + beforeAll(() => { + ({ buildGhReleaseCommand, resolveReleaseInputs } = require(releaseScriptPath)); + }); + + function createTempRepoRoot(): string { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-release-')); + fs.mkdirSync(path.join(tempRoot, 'docs', 'releases'), { recursive: true }); + return tempRoot; + } + + function writeFile(tempRoot: string, relativePath: string): string { + const absolutePath = path.join(tempRoot, relativePath); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, `${relativePath}\n`, 'utf8'); + return absolutePath; + } + + test('plans gh release create with required packaged assets and checked-in notes', () => { + const tempRoot = createTempRepoRoot(); + try { + writeFile(tempRoot, 'main.js'); + writeFile(tempRoot, 'manifest.json'); + writeFile(tempRoot, 'styles.css'); + writeFile(tempRoot, 'README.md'); + const notesFile = writeFile(tempRoot, path.join('docs', 'releases', '1.8.2.md')); + + const inputs = resolveReleaseInputs(tempRoot, '1.8.2'); + const command = buildGhReleaseCommand({ ...inputs, releaseExists: false }); + + expect(inputs).toEqual({ + tag: '1.8.2', + title: 'Notemd 1.8.2', + notesFile, + assets: [ + path.join(tempRoot, 'main.js'), + path.join(tempRoot, 'manifest.json'), + path.join(tempRoot, 'styles.css'), + path.join(tempRoot, 'README.md') + ] + }); + expect(command).toEqual([ + 'release', + 'create', + '1.8.2', + path.join(tempRoot, 'main.js'), + path.join(tempRoot, 'manifest.json'), + path.join(tempRoot, 'styles.css'), + path.join(tempRoot, 'README.md'), + '--title', + 'Notemd 1.8.2', + '--notes-file', + notesFile, + '--verify-tag' + ]); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + test('plans gh release upload with clobber when the release already exists', () => { + const command = buildGhReleaseCommand({ + tag: '1.8.2', + title: 'Notemd 1.8.2', + notesFile: '/tmp/ignored.md', + assets: ['/tmp/main.js', '/tmp/manifest.json', '/tmp/styles.css', '/tmp/README.md'], + releaseExists: true + }); + + expect(command).toEqual([ + 'release', + 'upload', + '1.8.2', + '/tmp/main.js', + '/tmp/manifest.json', + '/tmp/styles.css', + '/tmp/README.md', + '--clobber' + ]); + }); + + test('rejects missing packaged assets before invoking gh release', () => { + const tempRoot = createTempRepoRoot(); + try { + writeFile(tempRoot, 'main.js'); + writeFile(tempRoot, 'manifest.json'); + writeFile(tempRoot, 'styles.css'); + writeFile(tempRoot, path.join('docs', 'releases', '1.8.2.md')); + + expect(() => resolveReleaseInputs(tempRoot, '1.8.2')).toThrow( + path.join(tempRoot, 'README.md') + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + }); +});