feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -1,34 +1,274 @@
|
|||
// scripts/ci/sync-version.mjs
|
||||
// CI_COMMIT_TAG(v0.1.0-alpha 등)에서 버전을 추출해 apps/desktop/package.json에 기록한다.
|
||||
// electron-builder가 package.json version으로 설치파일명/latest.yml을 만들기 때문에
|
||||
// 태그와 산출물 버전이 어긋나지 않게 패키징 전에 실행한다.
|
||||
// Repository-wide product version SSOT synchronizer and release-tag gate.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ci/sync-version.mjs --check
|
||||
// node scripts/ci/sync-version.mjs --write
|
||||
// node scripts/ci/sync-version.mjs --check --tag v1.1.0
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
const tag = process.env.CI_COMMIT_TAG ?? process.argv[2]
|
||||
if (!tag) {
|
||||
console.log('[sync-version] CI_COMMIT_TAG 없음 — 버전 동기화 건너뜀')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const version = tag.replace(/^v/, '')
|
||||
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
console.error(`[sync-version] 유효하지 않은 semver: ${version} (tag: ${tag})`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const pkgPath = join(root, 'apps', 'desktop', 'package.json')
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
||||
const args = process.argv.slice(2)
|
||||
const write = args.includes('--write')
|
||||
const check = args.includes('--check') || !write
|
||||
|
||||
if (pkg.version === version) {
|
||||
console.log(`[sync-version] 이미 동기화됨: ${version}`)
|
||||
if (write && args.includes('--check')) {
|
||||
fail('Choose exactly one mode: --check or --write.')
|
||||
}
|
||||
|
||||
const explicitTagIndex = args.indexOf('--tag')
|
||||
if (explicitTagIndex !== -1 && !args[explicitTagIndex + 1]) {
|
||||
fail('--tag requires a value.')
|
||||
}
|
||||
|
||||
const explicitTag = explicitTagIndex === -1 ? undefined : args[explicitTagIndex + 1]
|
||||
const legacyTag = args.find(
|
||||
(arg, index) => !arg.startsWith('--') && index !== explicitTagIndex + 1,
|
||||
)
|
||||
const tag =
|
||||
explicitTag ??
|
||||
legacyTag ??
|
||||
process.env.CI_COMMIT_TAG ??
|
||||
(process.env.GITHUB_REF_NAME?.startsWith('v') ? process.env.GITHUB_REF_NAME : undefined)
|
||||
|
||||
const metadataPath = join(root, 'release', 'product-version.json')
|
||||
const metadata = readJson(metadataPath)
|
||||
assertExactKeys(metadata, [
|
||||
'schemaVersion',
|
||||
'version',
|
||||
'androidVersionCode',
|
||||
'iosBuildNumber',
|
||||
'releaseDate',
|
||||
'desktopLicensePublicKeyId',
|
||||
])
|
||||
|
||||
if (metadata.schemaVersion !== 1) fail('Unsupported product-version schemaVersion.')
|
||||
if (!isStableSemver(metadata.version)) fail(`Invalid stable version: ${metadata.version}`)
|
||||
if (
|
||||
!Number.isSafeInteger(metadata.androidVersionCode) ||
|
||||
metadata.androidVersionCode < 1 ||
|
||||
metadata.androidVersionCode > 2_100_000_000
|
||||
) {
|
||||
fail(`Invalid Android versionCode: ${metadata.androidVersionCode}`)
|
||||
}
|
||||
if (!Number.isSafeInteger(metadata.iosBuildNumber) || metadata.iosBuildNumber < 1) {
|
||||
fail(`Invalid iOS build number: ${metadata.iosBuildNumber}`)
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(metadata.releaseDate)) {
|
||||
fail(`Invalid releaseDate: ${metadata.releaseDate}`)
|
||||
}
|
||||
if (!/^[0-9a-f]{64}$/.test(metadata.desktopLicensePublicKeyId)) {
|
||||
fail(`Invalid desktopLicensePublicKeyId: ${metadata.desktopLicensePublicKeyId}`)
|
||||
}
|
||||
|
||||
if (tag !== undefined) {
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(tag)) fail(`Release tag must be stable semver: ${tag}`)
|
||||
if (tag !== `v${metadata.version}`) {
|
||||
fail(`Release tag ${tag} does not match product version v${metadata.version}.`)
|
||||
}
|
||||
}
|
||||
|
||||
const packageManifestPaths = [
|
||||
'package.json',
|
||||
'apps/admin/package.json',
|
||||
'apps/desktop/package.json',
|
||||
'apps/mobile/package.json',
|
||||
'apps/mobile-rn/package.json',
|
||||
'apps/web/package.json',
|
||||
'packages/api-client/package.json',
|
||||
'packages/core/package.json',
|
||||
'packages/i18n/package.json',
|
||||
'packages/ui/package.json',
|
||||
'packages/ui-native/package.json',
|
||||
'site/package.json',
|
||||
]
|
||||
|
||||
const changes = []
|
||||
|
||||
for (const path of packageManifestPaths) {
|
||||
updateJson(path, (document) => {
|
||||
document.version = metadata.version
|
||||
if (path === 'apps/desktop/package.json') {
|
||||
document.devDependencies.electron = '33.4.11'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateJson('package-lock.json', (document) => {
|
||||
document.version = metadata.version
|
||||
document.packages[''].version = metadata.version
|
||||
for (const path of packageManifestPaths.filter((path) => path !== 'site/package.json')) {
|
||||
const key = path === 'package.json' ? '' : path.replace(/\/package\.json$/, '')
|
||||
if (document.packages[key]) document.packages[key].version = metadata.version
|
||||
}
|
||||
if (document.packages['apps/desktop']?.devDependencies) {
|
||||
document.packages['apps/desktop'].devDependencies.electron = '33.4.11'
|
||||
}
|
||||
})
|
||||
|
||||
updateJson('apps/mobile-rn/package-lock.json', (document) => {
|
||||
document.version = metadata.version
|
||||
document.packages[''].version = metadata.version
|
||||
for (const key of [
|
||||
'../..',
|
||||
'../../packages/api-client',
|
||||
'../../packages/core',
|
||||
'../../packages/i18n',
|
||||
'../../packages/ui-native',
|
||||
]) {
|
||||
if (document.packages[key]) document.packages[key].version = metadata.version
|
||||
}
|
||||
})
|
||||
|
||||
if (existsSync(join(root, 'site', 'package-lock.json'))) {
|
||||
updateJson('site/package-lock.json', (document) => {
|
||||
document.version = metadata.version
|
||||
document.packages[''].version = metadata.version
|
||||
})
|
||||
}
|
||||
|
||||
updateText('apps/mobile/app.config.ts', (text) =>
|
||||
replaceExactlyOnce(text, /version: '[^']+',/, `version: '${metadata.version}',`, 'Expo version'),
|
||||
)
|
||||
|
||||
updateText('apps/mobile-rn/android/app/build.gradle', (text) => {
|
||||
let next = replaceExactlyOnce(
|
||||
text,
|
||||
/def resolvedVersionName = versionSettingsValid \? configuredVersionName : "[^"]+"/,
|
||||
`def resolvedVersionName = versionSettingsValid ? configuredVersionName : "${metadata.version}"`,
|
||||
'Android default versionName',
|
||||
)
|
||||
next = replaceExactlyOnce(
|
||||
next,
|
||||
/def resolvedVersionCode = versionSettingsValid \? configuredVersionCodeValue\.toInteger\(\) : \d+/,
|
||||
`def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : ${metadata.androidVersionCode}`,
|
||||
'Android default versionCode',
|
||||
)
|
||||
return next
|
||||
})
|
||||
|
||||
updateText('apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj', (text) => {
|
||||
const currentCount = countMatches(text, /CURRENT_PROJECT_VERSION = \d+;/g)
|
||||
const marketingCount = countMatches(text, /MARKETING_VERSION = [^;]+;/g)
|
||||
if (currentCount !== 2 || marketingCount !== 2) {
|
||||
fail(`Unexpected iOS version surface count: build=${currentCount}, marketing=${marketingCount}`)
|
||||
}
|
||||
return text
|
||||
.replace(/CURRENT_PROJECT_VERSION = \d+;/g, `CURRENT_PROJECT_VERSION = ${metadata.iosBuildNumber};`)
|
||||
.replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${metadata.version};`)
|
||||
})
|
||||
|
||||
updateText('apps/api-server/D3ROVoice.Api.csproj', (text) => {
|
||||
if (/<Version>[^<]+<\/Version>/.test(text)) {
|
||||
return text.replace(/<Version>[^<]+<\/Version>/, `<Version>${metadata.version}</Version>`)
|
||||
}
|
||||
return replaceExactlyOnce(
|
||||
text,
|
||||
/(<TargetFramework>[^<]+<\/TargetFramework>)/,
|
||||
`$1\n <Version>${metadata.version}</Version>`,
|
||||
'.NET product version anchor',
|
||||
)
|
||||
})
|
||||
|
||||
updateJson('apps/admin-swagger/openapi.json', (document) => {
|
||||
document.info.version = metadata.version
|
||||
})
|
||||
|
||||
updateText('apps/web/src/components/layout/sidebar.tsx', (text) =>
|
||||
replaceExactlyOnce(
|
||||
text,
|
||||
/\bv\d+\.\d+\.\d+\b/,
|
||||
`v${metadata.version}`,
|
||||
'Web sidebar product version',
|
||||
),
|
||||
)
|
||||
|
||||
const changelog = readFileSync(join(root, 'CHANGELOG.md'), 'utf8')
|
||||
if (!new RegExp(`^## \\[${escapeRegExp(metadata.version)}\\] - ${metadata.releaseDate}$`, 'm').test(changelog)) {
|
||||
fail(`CHANGELOG.md is missing [${metadata.version}] - ${metadata.releaseDate}.`)
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
process.stdout.write(
|
||||
`[version] GREEN ${metadata.version} (${metadata.androidVersionCode}/${metadata.iosBuildNumber})\n`,
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const prev = pkg.version
|
||||
pkg.version = version
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
||||
console.log(`[sync-version] apps/desktop version: ${prev} → ${version}`)
|
||||
if (check) {
|
||||
for (const change of changes) process.stderr.write(`[version] out of sync: ${change.path}\n`)
|
||||
fail('Run `npm run version:sync` and commit every synchronized surface.')
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
writeFileSync(change.absolutePath, change.content, 'utf8')
|
||||
process.stdout.write(`[version] synchronized ${change.path}\n`)
|
||||
}
|
||||
|
||||
process.stdout.write(`[version] synchronized product version ${metadata.version}\n`)
|
||||
|
||||
function updateJson(path, mutate) {
|
||||
const absolutePath = join(root, path)
|
||||
const current = readFileSync(absolutePath, 'utf8')
|
||||
const document = JSON.parse(current)
|
||||
mutate(document)
|
||||
const next = `${JSON.stringify(document, null, 2)}\n`
|
||||
recordChange(path, absolutePath, current, next)
|
||||
}
|
||||
|
||||
function updateText(path, mutate) {
|
||||
const absolutePath = join(root, path)
|
||||
const current = readFileSync(absolutePath, 'utf8')
|
||||
const next = mutate(current)
|
||||
recordChange(path, absolutePath, current, next)
|
||||
}
|
||||
|
||||
function recordChange(path, absolutePath, current, content) {
|
||||
if (content !== current) changes.push({ path, absolutePath, content })
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf8'))
|
||||
} catch (error) {
|
||||
fail(
|
||||
`Cannot parse ${relative(root, path)}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function assertExactKeys(value, expected) {
|
||||
const actual = Object.keys(value).sort()
|
||||
const wanted = [...expected].sort()
|
||||
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
||||
fail(`product-version keys must be exactly: ${wanted.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function replaceExactlyOnce(text, pattern, replacement, label) {
|
||||
const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`
|
||||
const matches = text.match(new RegExp(pattern.source, flags))
|
||||
if (matches?.length !== 1) {
|
||||
fail(`${label} must match exactly once; found ${matches?.length ?? 0}.`)
|
||||
}
|
||||
return text.replace(pattern, replacement)
|
||||
}
|
||||
|
||||
function countMatches(text, pattern) {
|
||||
return [...text.matchAll(pattern)].length
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function isStableSemver(value) {
|
||||
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value)
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`[version] ${message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue