mirror of
https://github.com/jacobinwwey/obsidian-NotEMD.git
synced 2026-07-22 05:48:27 +00:00
chore: harden github release asset publishing
This commit is contained in:
parent
350caf1c73
commit
bf3a4b30f8
6 changed files with 311 additions and 8 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
57
docs/releases/1.8.2.md
Normal 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 对齐与发布验证门禁强化。
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
102
scripts/release/publish-github-release.js
Normal file
102
scripts/release/publish-github-release.js
Normal 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
|
||||
};
|
||||
133
src/tests/githubReleaseWorkflow.test.ts
Normal file
133
src/tests/githubReleaseWorkflow.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue