159 lines
4.7 KiB
JavaScript
159 lines
4.7 KiB
JavaScript
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',
|
||
'HTTP_PROXY=',
|
||
'--build-arg',
|
||
'HTTPS_PROXY=',
|
||
'--build-arg',
|
||
'ALL_PROXY=',
|
||
'--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
|
||
})
|
||
}
|