feat(release): publish desktop updates from a tag through one feed
Desktop clients had two competing update sources: the runtime pointed at a legacy GitLab registry while the Forgejo packages were filled in by hardcoded, version-pinned scripts. Operators could not tell which feed was authoritative, and no release could be reproduced from a tag. Auto-update now reads a single canonical Forgejo registry feed, updated by a version-agnostic publisher that runs from the tag on Forgejo, GitLab, and GitHub CI alike. Channel, minimum supported version, forced install, full-versus-delta thresholds, staged rollout, and a remote kill switch come from one policy file the client fetches alongside the feed. Tag creation is gated on a clean tree, matching version surfaces, and a changelog section.
This commit is contained in:
parent
65ecc7aabc
commit
7953706142
21 changed files with 1619 additions and 90 deletions
77
scripts/ci/create-release-tag.mjs
Normal file
77
scripts/ci/create-release-tag.mjs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// scripts/ci/create-release-tag.mjs
|
||||
// 릴리스 태그 생성 게이트. 버전 SSOT·CHANGELOG·작업트리 상태를 검증한 뒤
|
||||
// annotated(기본) 또는 GPG 서명(--sign) 태그를 만든다.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ci/create-release-tag.mjs --dry-run
|
||||
// node scripts/ci/create-release-tag.mjs
|
||||
// node scripts/ci/create-release-tag.mjs --sign
|
||||
//
|
||||
// 태그는 절대 이동·삭제하지 않는다. 잘못된 릴리스는 더 높은 patch로 forward-fix한다.
|
||||
// 생성 후 `git push origin vX.Y.Z`로 push한다.
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const args = process.argv.slice(2)
|
||||
const dryRun = args.includes('--dry-run')
|
||||
const sign = args.includes('--sign') || args.includes('-s')
|
||||
|
||||
const metadata = JSON.parse(readFileSync(join(root, 'release', 'product-version.json'), 'utf8'))
|
||||
if (!/^\d+\.\d+\.\d+$/.test(metadata.version)) {
|
||||
fail(`Only stable semver can be tagged: ${metadata.version}`)
|
||||
}
|
||||
const tag = `v${metadata.version}`
|
||||
|
||||
const changelog = readFileSync(join(root, 'CHANGELOG.md'), 'utf8')
|
||||
const header = `## [${metadata.version}] - ${metadata.releaseDate}`
|
||||
if (!changelog.includes(header)) {
|
||||
fail(`CHANGELOG.md is missing the release section: "${header}"`)
|
||||
}
|
||||
|
||||
const status = git(['status', '--porcelain'])
|
||||
if (status.stdout.trim() && !args.includes('--allow-dirty')) {
|
||||
fail('Working tree is dirty. Commit release surfaces first, or pass --allow-dirty for a local dry tag.')
|
||||
}
|
||||
|
||||
const existing = git(['tag', '--list', tag]).stdout.trim()
|
||||
if (existing) {
|
||||
fail(`Tag ${tag} already exists. Releases are immutable — lift the version and re-tag.`)
|
||||
}
|
||||
|
||||
const head = git(['rev-parse', 'HEAD']).stdout.trim()
|
||||
const message = `Release ${tag}`
|
||||
const tagArgs = sign
|
||||
? ['tag', '-s', tag, '-m', message]
|
||||
: ['tag', '-a', tag, '-m', message]
|
||||
|
||||
if (dryRun) {
|
||||
process.stdout.write(
|
||||
`[tag] dry-run — would create ${sign ? 'signed' : 'annotated'} ${tag} at ${head}\n` +
|
||||
`[tag] next: git push origin ${tag}\n`,
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const result = spawnSync('git', tagArgs, { cwd: root, stdio: 'inherit' })
|
||||
if (result.status !== 0) {
|
||||
fail(`git ${tagArgs.join(' ')} failed (exit ${result.status})`)
|
||||
}
|
||||
|
||||
process.stdout.write(`[tag] created ${tag}. Push with: git push origin ${tag}\n`)
|
||||
|
||||
function git(commandArgs) {
|
||||
const result = spawnSync('git', commandArgs, { cwd: root, encoding: 'utf8' })
|
||||
if (result.status !== 0) {
|
||||
fail(`git ${commandArgs.join(' ')} failed: ${result.stderr?.trim() || `exit ${result.status}`}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`[tag] ${message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue