feat: 支持跨平台前端镜像部署
This commit is contained in:
152
scripts/deploy-frontend.mjs
Normal file
152
scripts/deploy-frontend.mjs
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user