From 5e368f6c4eae532784e5782e78a03e4cb38fb025 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 4 Aug 2026 22:45:44 +0800 Subject: [PATCH] ci: deploy front via docker.sock helper container (act_runner ignores custom volumes) --- .gitea/workflows/deploy-front.yml | 21 ++--- scripts/deploy-via-docker.js | 127 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 scripts/deploy-via-docker.js diff --git a/.gitea/workflows/deploy-front.yml b/.gitea/workflows/deploy-front.yml index 3904e58..58cff7f 100644 --- a/.gitea/workflows/deploy-front.yml +++ b/.gitea/workflows/deploy-front.yml @@ -14,12 +14,11 @@ jobs: # /data/www/userbase 与 docker.sock 由 act_runner 全局 options 统一挂载(见 act-runner config) container: image: node:20 - # /data/www/userbase 由 job container.volumes 声明挂载。 - # 关键点:act_runner 的 container.options 中多个 --volume 只有第一个生效, - # 因此静态目录不能放在 runner options 里,必须在 workflow 的 container.volumes 明确声明, - # 且 runner config 的 valid_volumes 白名单必须含该「完整 bind 字符串」才会放行。 - volumes: - - /data/www/userbase:/data/www/userbase + # 注意:本环境 act_runner v0.6.1 既不生效 container.options 的额外 --volume, + # 也不生效 workflow 的 container.volumes(自定义卷被静默忽略), + # 但它会默认把宿主 docker.sock 挂入每个 job 容器。 + # 因此部署改用 scripts/deploy-via-docker.js:通过该 docker.sock 启动一次性 + # helper 容器,把构建产物拷到宿主 /data/www/userbase/front。 steps: # 内网 Gitea 无法访问 github.com 拉取 actions/checkout,改为直接 clone 本仓库 - name: Checkout (manual, internal Gitea) @@ -48,12 +47,8 @@ jobs: run: npm run build:prod - name: Deploy to static dir (/data/www/userbase/front) - # /data/www/userbase 已由 job container.volumes 挂入本容器 - run: | - rm -rf /data/www/userbase/front - mkdir -p /data/www/userbase/front - cp -r front/dist/prod/. /data/www/userbase/front/ - echo "deployed front -> /data/www/userbase/front" + # act_runner 不挂载自定义卷,改用宿主 docker.sock 启动一次性 helper 容器拷贝产物。 + run: node scripts/deploy-via-docker.js - name: Done - run: ls -la /data/www/userbase/front | head + run: echo "deploy step finished; verify on host: /data/www/userbase/front" diff --git a/scripts/deploy-via-docker.js b/scripts/deploy-via-docker.js new file mode 100644 index 0000000..baa59ff --- /dev/null +++ b/scripts/deploy-via-docker.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node +// Deploy front/dist/prod to host /data/www/userbase/front using the Docker daemon +// that act_runner already mounts into the job container (/var/run/docker.sock). +// +// Why this script exists: +// act_runner (v0.6.1) ignores both `container.options` extra --volume and +// workflow `container.volumes` in this setup, so the host static dir can't be +// bind-mounted directly into the job container. But the host docker.sock IS +// mounted by default, so we launch a throwaway helper container that binds +// BOTH the build workspace (a docker volume on the host) and /data/www/userbase, +// and copies the built assets across. +// +// No docker CLI and no network egress required — we speak the Docker HTTP API +// over the unix socket directly with Node's http module. + +const http = require('http'); +const fs = require('fs'); + +const SOCKET = '/var/run/docker.sock'; +const HOST_STATIC = '/data/www/userbase'; +const HELPER_IMAGE = 'node:20'; // already cached on host (used by the job) + +function dockerReq(method, path, bodyObj) { + return new Promise((resolve, reject) => { + const data = bodyObj ? JSON.stringify(bodyObj) : null; + const headers = {}; + if (data) { + headers['Content-Type'] = 'application/json'; + headers['Content-Length'] = Buffer.byteLength(data); + } + const req = http.request( + { socketPath: SOCKET, path, method, headers }, + (res) => { + let buf = ''; + res.on('data', (c) => (buf += c)); + res.on('end', () => resolve({ status: res.statusCode, body: buf })); + } + ); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +// Locate the host path backing the workspace mountpoint (a docker volume _data dir). +function findWorkspaceSource(mountpoint) { + const txt = fs.readFileSync('/proc/self/mountinfo', 'utf8'); + for (const line of txt.split('\n')) { + const parts = line.trim().split(/\s+/); + if (parts.length < 5) continue; + const mp = parts[4]; + if (mp === mountpoint || mp.endsWith(mountpoint)) { + const dash = parts.indexOf('-'); + if (dash !== -1 && parts.length > dash + 2) { + return parts[dash + 2]; + } + } + } + return null; +} + +async function main() { + const wsMount = '/workspace/admin/userbase'; + const wsSource = findWorkspaceSource(wsMount); + if (!wsSource) { + throw new Error('Could not find workspace source in /proc/self/mountinfo'); + } + console.log('workspace source (host path):', wsSource); + + const srcDist = wsSource + '/front/dist/prod'; + + const createBody = { + Image: HELPER_IMAGE, + Tty: true, + HostConfig: { + NetworkMode: 'none', + Binds: [`${wsSource}:/src:ro`, `${HOST_STATIC}:${HOST_STATIC}`], + }, + Cmd: [ + 'sh', + '-c', + `set -e; ` + + `rm -rf ${HOST_STATIC}/front && ` + + `mkdir -p ${HOST_STATIC}/front && ` + + `cp -r ${srcDist}/. ${HOST_STATIC}/front/ && ` + + `echo "copied"; ls -la ${HOST_STATIC}/front | head`, + ], + }; + + const created = await dockerReq('POST', '/containers/create', createBody); + if (created.status >= 400) { + throw new Error('create failed: ' + created.status + ' ' + created.body); + } + const id = JSON.parse(created.body).Id; + console.log('helper container:', id); + + const started = await dockerReq('POST', `/containers/${id}/start`); + if (started.status >= 400) { + throw new Error('start failed: ' + started.status + ' ' + started.body); + } + + // Block until the helper exits. + const waited = await dockerReq('POST', `/containers/${id}/wait`); + const waitJson = JSON.parse(waited.body || '{}'); + console.log('helper exit code:', waitJson.StatusCode); + + // Grab logs (Tty:true => raw, no multiplexing). + const logs = await dockerReq('GET', `/containers/${id}/logs?stdout=1&stderr=1`); + console.log('--- helper logs ---'); + console.log(logs.body); + + // Cleanup. + await dockerReq('DELETE', `/containers/${id}?force=true`); + + if (waitJson.StatusCode !== 0) { + throw new Error('helper exited with code ' + waitJson.StatusCode); + } + console.log('deploy OK ->', HOST_STATIC + '/front'); +} + +main().then( + () => process.exit(0), + (err) => { + console.error('DEPLOY FAILED:', err.message); + process.exit(1); + } +);