fix(release): stop shipping native modules built for the wrong runtime
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
The released installer could not start: it carried a better-sqlite3 build for the host Node runtime instead of Electron, so the app died immediately with a module version mismatch when it opened its database. Packaging now proves the Electron build of every runtime-sensitive native module before an installer or archive exists, and installers are produced only from that verified tree, so the mistake cannot pass silently. The release pipelines run the same check. The default local model also pointed at a retired model: a *.gguf name that Ollama cannot serve, while the settings, onboarding, and guide screens recommended an older model. All of them now use the model the service code already preferred.
This commit is contained in:
parent
0fbbbc1756
commit
1af3cf75c7
42 changed files with 473 additions and 83 deletions
|
|
@ -133,28 +133,51 @@ async function forgejoFetch(url, init = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 원격 파일이 로컬 바이트와 같은지 판단한다.
|
||||
* Forgejo generic registry는 HEAD를 405로 거부하고 해시도 주지 않으므로,
|
||||
* Range GET으로 크기를 본 뒤 1MiB 이하는 실제 바이트까지 비교한다.
|
||||
* (크기만 비교하면 버전 문자열만 바뀐 latest.yml 같은 메타데이터를 놓친다 — 실측 사고.)
|
||||
*/
|
||||
async function remoteIsIdentical(url, body, fetchImpl) {
|
||||
const probe = await fetchImpl(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (!probe?.ok) return false
|
||||
const contentRange = probe.headers.get('content-range')
|
||||
const remoteSize = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (!Number.isFinite(remoteSize) || remoteSize !== body.length) return false
|
||||
if (body.length > 1024 * 1024) return true
|
||||
const full = await fetchImpl(url).catch(() => null)
|
||||
if (!full?.ok) return false
|
||||
const remoteBytes = Buffer.from(await full.arrayBuffer())
|
||||
return remoteBytes.length === body.length && remoteBytes.equals(body)
|
||||
}
|
||||
|
||||
async function upload(url, body, contentType) {
|
||||
if (check) {
|
||||
console.log(`[portable] (check) PUT ${url} (${body.length} bytes)`)
|
||||
return
|
||||
}
|
||||
// Forgejo의 generic registry는 HEAD를 405로 거부한다(실측) → Range GET으로 크기만 읽는다.
|
||||
const probe = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
const contentRange = probe?.headers.get('content-range')
|
||||
const remoteLength = contentRange ? Number(contentRange.split('/')[1]) : NaN
|
||||
if (probe?.ok && Number.isFinite(remoteLength)) {
|
||||
if (remoteLength === body.length) {
|
||||
console.log(`[portable] 이미 동일한 파일이 있습니다(건너뜀): ${url}`)
|
||||
return
|
||||
}
|
||||
// 볼륨은 불변 자산이다 — 같은 버전 경로에 다른 바이트가 있으면 덮어쓰지 않고 중단한다.
|
||||
if (url.includes(`/portable-${version}/`) && url.includes('.7z.')) {
|
||||
|
||||
// 메타데이터는 크기가 아니라 내용까지 비교해야 한다.
|
||||
// 크기만 보면 버전 문자열만 바뀐 latest.yml/json을 "동일"로 오판한다 — 실측 사고.
|
||||
if (await remoteIsIdentical(url, body, forgejoFetch)) {
|
||||
console.log(`[portable] 이미 동일한 파일이 있습니다(건너): ${url}`)
|
||||
return
|
||||
}
|
||||
|
||||
const existing = await forgejoFetch(url, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||
if (existing?.ok) {
|
||||
// 볼륨/부품은 불변 자산이다 — 같은 버전 경로에 다른 바이트가 있으면 덮어쓰지 않고 중단한다.
|
||||
const isImmutableAsset =
|
||||
url.includes(`/portable-${version}/`) && (url.includes('.7z.') || url.includes('.zip.'))
|
||||
if (isImmutableAsset) {
|
||||
console.error(
|
||||
`[portable] ${version} 볼륨에 다른 바이트가 이미 있습니다: ${url}\n` +
|
||||
' 이미 게시된 버전은 덮어쓰지 않습니다(불변). 새 버전으로 게시하세요.',
|
||||
`[portable] ${version} 자산에 다른 바이트가 이미 있습니다: ${url}\n` +
|
||||
' 이미 게시된 버전은 어쓰지 않습니다(불변). 새 버전으로 게시하세요.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
// 메타데이터와 latest 별칭은 최신을 반영해야 하므로 지우고 쓴다.
|
||||
await forgejoFetch(url, { method: 'DELETE' }).catch(() => null)
|
||||
}
|
||||
const response = await forgejoFetch(url, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue