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.
77 lines
2.7 KiB
JavaScript
77 lines
2.7 KiB
JavaScript
// 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)
|
|
}
|