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 }) }