Files
OpenTerminal/scripts/archive-releases.cjs
T
Bill 5ff311ea0f docs+chore: refresh README/STATE, cache electron in CI, lock chord by keycode, misc P3
- README: add lock-screen section, refresh test list (13 offline tests),
  updater fallback order, data locations, architecture tree
- STATE.md: v1.0.20, current release.cjs flow (no --skip-github), M11-M13
- ci.yml: actions/cache for the ~100MB Electron binary keyed on lockfile
- .gitignore: ignore .env.* but keep committed templates
- scripts/archive-releases.cjs moved in from tmp-test; KEEP_TAG is now a
  required CLI arg (a hardcoded default is how the wrong version gets wiped)
- lockShortcuts: isPanicLockChord matches input.code ('KeyL') so Dvorak and
  non-Latin layouts trigger Ctrl+L lock correctly
- settings: drop the dead single-option update-channel Select from About tab
- Workspace/App: memo(IconRail/LayoutFlyout), useMemo layoutMenu keyed on
  language, useCallback onOpenSettings; drop unused IconRail onSplit prop
2026-10-07 22:06:58 +08:00

99 lines
4.0 KiB
JavaScript

/* Archive old Gitea releases: backup assets locally, then delete remote
releases, keeping only the one you name. Tags are untouched.
WARNING: `delete` removes every release except <keep-tag> from the Gitea
repository. The kept tag is a REQUIRED argument precisely because a
hardcoded default is how the wrong version gets wiped — the script refuses
to run without it. `delete` is safe by construction: it aborts unless every
asset of every old release already has a size-matching local backup.
Usage:
node scripts/archive-releases.cjs <keep-tag> backup # download assets only
node scripts/archive-releases.cjs <keep-tag> delete # delete releases whose
# assets are all backed
# up (size match)
Reads GIT_TOKEN from .env (same format as scripts/release.cjs). */
const fs = require('node:fs')
const path = require('node:path')
const ROOT = path.join(__dirname, '..')
const KEEP_TAG = process.argv[2]
if (!KEEP_TAG) {
console.error('usage: node scripts/archive-releases.cjs <keep-tag> backup|delete')
process.exit(1)
}
const ARCHIVE = path.join(ROOT, 'release', 'archive')
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 API = 'https://git.codingplan.site/api/v1/repos/admin/OpenTerminal'
const H = { Authorization: `token ${env.GIT_TOKEN}` }
const listReleases = async () => {
const out = []
let page = 1
for (;;) {
const res = await fetch(`${API}/releases?limit=50&page=${page}`, { headers: H })
if (!res.ok) throw new Error(`list HTTP ${res.status}`)
const batch = await res.json()
if (!batch.length) break
out.push(...batch)
page++
}
return out
}
const backup = async () => {
const releases = await listReleases()
const old = releases.filter((r) => r.tag_name !== KEEP_TAG)
let ok = true
for (const r of old) {
for (const a of r.assets ?? []) {
const dir = path.join(ARCHIVE, r.tag_name)
fs.mkdirSync(dir, { recursive: true })
const dest = path.join(dir, a.name)
if (fs.existsSync(dest) && fs.statSync(dest).size === a.size) {
console.log(`skip (have) ${r.tag_name}/${a.name}`)
continue
}
process.stdout.write(`get ${r.tag_name}/${a.name} (${(a.size / 1e6).toFixed(1)} MB) ... `)
const res = await fetch(a.browser_download_url, { headers: H })
if (!res.ok) { console.log(`HTTP ${res.status}`); ok = false; continue }
const buf = Buffer.from(await res.arrayBuffer())
if (buf.length !== a.size) { console.log(`size mismatch ${buf.length} != ${a.size}`); ok = false; continue }
fs.writeFileSync(dest, buf)
console.log('ok')
}
}
console.log(ok ? 'BACKUP DONE' : 'BACKUP INCOMPLETE — do not delete')
}
const del = async () => {
const releases = await listReleases()
const old = releases.filter((r) => r.tag_name !== KEEP_TAG)
let ok = true
for (const r of old) {
for (const a of r.assets ?? []) {
const dest = path.join(ARCHIVE, r.tag_name, a.name)
const have = fs.existsSync(dest) && fs.statSync(dest).size === a.size
if (!have) { console.log(`MISSING backup ${r.tag_name}/${a.name} — abort this release`); ok = false }
}
}
if (!ok) { console.log('DELETE ABORTED'); return }
for (const r of old) {
const res = await fetch(`${API}/releases/${r.id}`, { method: 'DELETE', headers: H })
console.log(`delete ${r.tag_name}: HTTP ${res.status}`)
}
const left = await listReleases()
console.log(`remaining: ${left.map((r) => r.tag_name).join(', ')}`)
}
;(async () => {
const mode = process.argv[3]
if (mode === 'backup') await backup()
else if (mode === 'delete') await del()
else console.log('usage: node scripts/archive-releases.cjs <keep-tag> backup|delete')
})().catch((e) => { console.error('failed:', e.message); process.exit(1) })