feat: 支持跨平台前端镜像部署

This commit is contained in:
2026-09-20 15:25:04 +08:00
parent cb1561a317
commit 55eec78a90
6 changed files with 210 additions and 75 deletions

152
scripts/deploy-frontend.mjs Normal file
View File

@@ -0,0 +1,152 @@
import { execFile } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { promisify } from 'node:util'
const execFileAsync = promisify(execFile)
export const REGISTRY = process.env.FRONTEND_REGISTRY ?? 'registry.boss160.cn'
export const IMAGE_NAME = process.env.FRONTEND_IMAGE ?? `${REGISTRY}/junhong/cmp-admin-web`
export const NODE_OPTIONS = process.env.FRONTEND_NODE_OPTIONS ?? '--max-old-space-size=1536'
const ZERO_SHA = '0'.repeat(40)
export function parseArguments(args) {
if (args.length === 1 && args[0] === '--pre-push') {
return { mode: 'pre-push' }
}
if (args[0] === '--branch' && args.length === 2 && args[1]) {
return { mode: 'branch', branch: args[1] }
}
if (args.length === 0) {
throw new Error('缺少参数:Hook 使用 --pre-push,手动构建使用 --branch <分支>')
}
throw new Error(`未知参数:${args.join(' ')}`)
}
export function parsePrePushLines(input) {
return input
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const fields = line.split(/\s+/)
if (fields.length !== 4) {
throw new Error(`无法解析 pre-push 数据:${line}`)
}
const [localRef, localSha, remoteRef, remoteSha] = fields
return { localRef, localSha, remoteRef, remoteSha }
})
}
export function resolveImageTag(sha) {
if (!/^[0-9a-f]{40}$/i.test(sha)) {
throw new Error(`不是完整 commit SHA:${sha}`)
}
return sha.toLowerCase()
}
function branchFromRef(ref) {
if (!ref.startsWith('refs/heads/')) {
throw new Error(`只允许推送分支,收到:${ref}`)
}
const branch = ref.slice('refs/heads/'.length)
if (!/^[A-Za-z0-9._/-]+$/.test(branch) || branch.includes('..') || branch.startsWith('-')) {
throw new Error(`非法分支名:${branch}`)
}
return branch
}
async function run(command, args, options = {}) {
const display = [command, ...args].join(' ')
console.log(`$ ${display}`)
return execFileAsync(command, args, {
cwd: options.cwd,
env: options.env,
maxBuffer: 10 * 1024 * 1024
})
}
async function readStdin() {
const chunks = []
for await (const chunk of process.stdin) chunks.push(chunk)
return Buffer.concat(chunks).toString('utf8')
}
async function resolveBranchCommit(branch) {
branchFromRef(`refs/heads/${branch}`)
await run('git', [
'fetch',
'--quiet',
'origin',
`refs/heads/${branch}:refs/remotes/origin/${branch}`
])
const { stdout } = await run('git', ['rev-parse', `refs/remotes/origin/${branch}`])
return { branch, sha: resolveImageTag(stdout.trim()) }
}
async function buildAndPush({ branch, sha, repoRoot }) {
const imageTag = resolveImageTag(sha)
const tempDir = await mkdtemp(join(tmpdir(), 'one-pipe-frontend-'))
try {
await run('git', ['worktree', 'add', '--detach', tempDir, imageTag], { cwd: repoRoot })
await run(
'docker',
[
'buildx',
'build',
'--platform',
'linux/amd64',
'--build-arg',
`NODE_OPTIONS=${NODE_OPTIONS}`,
'--tag',
`${IMAGE_NAME}:${imageTag}`,
'--push',
tempDir
],
{ cwd: repoRoot }
)
console.log(`镜像已推送:${IMAGE_NAME}:${imageTag}`)
console.log(`分支:${branch},提交:${imageTag}`)
} finally {
await run('git', ['worktree', 'remove', '--force', tempDir], { cwd: repoRoot }).catch(() => {})
await rm(tempDir, { recursive: true, force: true })
}
}
async function repoRoot() {
const { stdout } = await run('git', ['rev-parse', '--show-toplevel'])
return resolve(stdout.trim())
}
async function main() {
const options = parseArguments(process.argv.slice(2))
const root = await repoRoot()
if (options.mode === 'pre-push') {
const refs = parsePrePushLines(await readStdin())
for (const ref of refs) {
if (ref.remoteSha === ZERO_SHA || ref.localSha === ZERO_SHA) continue
const branch = branchFromRef(ref.remoteRef)
await buildAndPush({ branch, sha: ref.localSha, repoRoot: root })
}
return
}
const target = await resolveBranchCommit(options.branch)
await buildAndPush({ ...target, repoRoot: root })
console.log('手动构建完成。要触发 Gitea 自动部署,请将该分支对应提交推送到远程。')
}
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
main().catch((error) => {
console.error(`前端镜像部署失败:${error.message}`)
process.exitCode = 1
})
}