From dc3d54fa07e9892e1835a1e35c72b64f6b7f9c15 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 8 Sep 2026 01:15:02 +0800 Subject: [PATCH] chore: release script in repo + AGENTS.md dev/release docs --- .gitignore | 3 ++ AGENTS.md | 37 ++++++++++++++ scripts/release.cjs | 117 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 AGENTS.md create mode 100644 scripts/release.cjs diff --git a/.gitignore b/.gitignore index b427c64..814619f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ release/ # stray local test artifacts .min-test.bin .min2-test.bin + +# release notes draft (consumed by scripts/release.cjs) +RELEASE_NOTES.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..304cbc3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# OpenTerminal 开发备忘 + +Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。 + +## 常用命令 + +- 开发:`npm run dev`(主进程改动不热重建,需重启) +- 类型检查:`npx tsc --noEmit -p tsconfig.web.json` +- 打包:`npm run dist`,产物在 `release/`(msi + exe + latest.yml + blockmap) + - 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist` + +## 仓库与远程 + +- `origin` = Gitea `https://git.codingplan.site/admin/OpenTerminal.git`(主仓库 + 更新通道) +- `github` = `https://github.com/billowliu2/OpenTerminal.git`(镜像 + 备用更新源) +- ⚠️ Gitea 的 upload-pack 有故障,`git fetch origin` 会报 `bad pack header`;查询远端状态用 API 或 `git ls-remote`。推送正常。 +- 2026-09:远端 main 旧历史(M3–M8,至 v0.8.0 d3a628f)被 force-push 覆盖为当前历史,旧提交仍由 tag v0.6.0 / v0.7.0 / v0.8.0 保留。 + +## 凭据 + +- `.env`(已 gitignore):`GIT_TOKEN`(Gitea)、`GH_TOKEN`(GitHub)等。git 推送通过 credential helper 使用该令牌。 + +## 发布新版本 + +1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore) +2. `npm run dist` 构建 +3. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.2` + - 依次完成:Gitea release(含 msi/exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`,更新 latest.yml/blockmap/exe)→ GitHub release + - 可用 `--skip-github` / `--skip-gitea` 跳过某步 + - 访问 GitHub API 不通时,设 `HTTPS_PROXY=http://127.0.0.1:7897` 再走代理 +4. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号 +5. `git tag v<版本号>` 并推送两个远程 + +## 主题机制 + +- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生 +- 标签强调色:`settings.tabAccentColor`(默认 `#3fb950`),经 `--tab-accent` CSS 变量生效 diff --git a/scripts/release.cjs b/scripts/release.cjs new file mode 100644 index 0000000..a79e273 --- /dev/null +++ b/scripts/release.cjs @@ -0,0 +1,117 @@ +// OpenTerminal release publisher +// Usage: node scripts/release.cjs [--skip-github] [--skip-gitea] +// node scripts/release.cjs 1.0.2 +// Reads release notes from RELEASE_NOTES.md (repo root, gitignored). +// Credentials from .env (repo root, gitignored): GIT_TOKEN, GH_TOKEN. +// If HTTPS_PROXY / HTTP_PROXY (env or .env) is set, traffic goes through it +// (needed for api.github.com / uploads.github.com in some networks). +const fs = require('node:fs') +const path = require('node:path') + +const ROOT = path.join(__dirname, '..') +const V = process.argv[2] +if (!V || !/^\d+\.\d+\.\d+$/.test(V)) { + console.error('usage: node scripts/release.cjs [--skip-github] [--skip-gitea]') + process.exit(1) +} +const SKIP_GH = process.argv.includes('--skip-github') +const SKIP_GITEA = process.argv.includes('--skip-gitea') + +const env = Object.fromEntries( + fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n') + .filter((l) => l.includes('=') && !l.startsWith('#')) + .map((l) => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()] }) +) +const notes = fs.readFileSync(path.join(ROOT, 'RELEASE_NOTES.md'), 'utf8') +const R = path.join(ROOT, 'release') +const files = [ + path.join(R, `OpenTerminal-${V}-setup.msi`), + path.join(R, `OpenTerminal-${V}-setup.exe`) +] +for (const f of files) { + if (!fs.existsSync(f)) { console.error('missing build artifact:', f); process.exit(1) } +} + +const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || env.HTTPS_PROXY || env.PROXY +if (proxy) { + const { ProxyAgent, setGlobalDispatcher } = require('undici') + setGlobalDispatcher(new ProxyAgent(proxy)) + console.log('using proxy:', proxy) +} + +async function upload(url, file, token, authScheme) { + const name = path.basename(file) + const stat = fs.statSync(file) + const resp = await fetch(`${url}${url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name)}`, { + method: 'POST', + headers: { + Authorization: `${authScheme} ${token}`, + 'Content-Type': 'application/octet-stream', + 'Content-Length': String(stat.size) + }, + body: fs.createReadStream(file), + duplex: 'half' + }) + console.log(` upload ${name}: HTTP ${resp.status}`) + if (!resp.ok) console.log(' ', (await resp.text()).slice(0, 300)) +} + +async function gitea() { + console.log('=== Gitea release ===') + const base = 'https://git.codingplan.site/api/v1/repos/admin/OpenTerminal' + const resp = await fetch(`${base}/releases`, { + method: 'POST', + headers: { Authorization: `token ${env.GIT_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ tag_name: `v${V}`, name: `OpenTerminal v${V}`, body: notes, draft: false, prerelease: false }) + }) + if (!resp.ok) { console.log('create failed:', resp.status, (await resp.text()).slice(0, 300)); return } + const rel = await resp.json() + console.log('release created, id =', rel.id) + for (const f of files) await upload(`${base}/releases/${rel.id}/assets`, f, env.GIT_TOKEN, 'token') +} + +async function giteaChannel() { + console.log('=== Gitea update channel ===') + const base = 'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable' + const del = await fetch(base, { method: 'DELETE', headers: { Authorization: `token ${env.GIT_TOKEN}` } }) + console.log(' delete old stable:', del.status) + for (const f of [path.join(R, 'latest.yml'), path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), path.join(R, `OpenTerminal-${V}-setup.exe`)]) { + const name = path.basename(f) + const stat = fs.statSync(f) + const resp = await fetch(`${base}/${encodeURIComponent(name)}`, { + method: 'PUT', + headers: { Authorization: `token ${env.GIT_TOKEN}`, 'Content-Type': 'application/octet-stream', 'Content-Length': String(stat.size) }, + body: fs.createReadStream(f), + duplex: 'half' + }) + console.log(` put ${name}: HTTP ${resp.status}`) + } +} + +async function github() { + console.log('=== GitHub release ===') + const resp = await fetch('https://api.github.com/repos/billowliu2/OpenTerminal/releases', { + method: 'POST', + headers: { + Authorization: `Bearer ${env.GH_TOKEN}`, + 'Content-Type': 'application/json', + 'User-Agent': 'OpenTerminal', + Accept: 'application/vnd.github+json' + }, + body: JSON.stringify({ tag_name: `v${V}`, name: `OpenTerminal v${V}`, body: notes, draft: false, prerelease: false }) + }) + if (!resp.ok) { console.log('create failed:', resp.status, (await resp.text()).slice(0, 300)); return } + const rel = await resp.json() + console.log('release created, id =', rel.id) + const up = `https://uploads.github.com/repos/billowliu2/OpenTerminal/releases/${rel.id}/assets` + for (const f of [...files, path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), path.join(R, 'latest.yml')]) { + await upload(up, f, env.GH_TOKEN, 'Bearer') + } +} + +async function main() { + if (!SKIP_GITEA) { await gitea(); await giteaChannel() } + if (!SKIP_GH) await github() + console.log('done') +} +main().catch((e) => { console.error(e); process.exit(1) })