chore: harden github release asset publishing

This commit is contained in:
Jacobinwwey 2026-04-14 00:16:03 -05:00
parent 350caf1c73
commit bf3a4b30f8
6 changed files with 311 additions and 8 deletions

View file

@ -56,10 +56,15 @@ Required release assets:
- `styles.css`
- `README.md`
## 5. Suggested Release Command Pattern
## 5. Publish Command
```bash
gh release create <tag> main.js manifest.json styles.css README.md \
--title "Notemd <tag>" \
--notes-file docs/releases/<tag>.md
npm run release:github -- <tag>
```
The helper enforces the required packaged assets and `docs/releases/<tag>.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.

View file

@ -56,10 +56,15 @@ Release 必需资产:
- `styles.css`
- `README.md`
## 5. 推荐发布命令模板
## 5. 发布命令
```bash
gh release create <tag> main.js manifest.json styles.css README.md \
--title "Notemd <tag>" \
--notes-file docs/releases/<tag>.md
npm run release:github -- <tag>
```
该辅助命令会在调用 GitHub 前强制校验必须打包的资产以及 `docs/releases/<tag>.md`
- 如果 release 尚不存在,则执行 `gh release create ... --verify-tag`
- 如果 release 已存在,则执行 `gh release upload ... --clobber`
第二条路径就是这类问题的修复路径release 文案已发布,但插件安装资产未上传时,可直接补传。

57
docs/releases/1.8.2.md Normal file
View file

@ -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 对齐与发布验证门禁强化。

View file

@ -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"
},

View file

@ -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 <tag> [--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
};

View file

@ -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 });
}
});
});
});