Archived
forked from admin/userbase
ci: deploy front via docker.sock helper container (act_runner ignores custom volumes)
This commit is contained in:
@@ -14,12 +14,11 @@ jobs:
|
|||||||
# /data/www/userbase 与 docker.sock 由 act_runner 全局 options 统一挂载(见 act-runner config)
|
# /data/www/userbase 与 docker.sock 由 act_runner 全局 options 统一挂载(见 act-runner config)
|
||||||
container:
|
container:
|
||||||
image: node:20
|
image: node:20
|
||||||
# /data/www/userbase 由 job container.volumes 声明挂载。
|
# 注意:本环境 act_runner v0.6.1 既不生效 container.options 的额外 --volume,
|
||||||
# 关键点:act_runner 的 container.options 中多个 --volume 只有第一个生效,
|
# 也不生效 workflow 的 container.volumes(自定义卷被静默忽略),
|
||||||
# 因此静态目录不能放在 runner options 里,必须在 workflow 的 container.volumes 明确声明,
|
# 但它会默认把宿主 docker.sock 挂入每个 job 容器。
|
||||||
# 且 runner config 的 valid_volumes 白名单必须含该「完整 bind 字符串」才会放行。
|
# 因此部署改用 scripts/deploy-via-docker.js:通过该 docker.sock 启动一次性
|
||||||
volumes:
|
# helper 容器,把构建产物拷到宿主 /data/www/userbase/front。
|
||||||
- /data/www/userbase:/data/www/userbase
|
|
||||||
steps:
|
steps:
|
||||||
# 内网 Gitea 无法访问 github.com 拉取 actions/checkout,改为直接 clone 本仓库
|
# 内网 Gitea 无法访问 github.com 拉取 actions/checkout,改为直接 clone 本仓库
|
||||||
- name: Checkout (manual, internal Gitea)
|
- name: Checkout (manual, internal Gitea)
|
||||||
@@ -48,12 +47,8 @@ jobs:
|
|||||||
run: npm run build:prod
|
run: npm run build:prod
|
||||||
|
|
||||||
- name: Deploy to static dir (/data/www/userbase/front)
|
- name: Deploy to static dir (/data/www/userbase/front)
|
||||||
# /data/www/userbase 已由 job container.volumes 挂入本容器
|
# act_runner 不挂载自定义卷,改用宿主 docker.sock 启动一次性 helper 容器拷贝产物。
|
||||||
run: |
|
run: node scripts/deploy-via-docker.js
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Done
|
- name: Done
|
||||||
run: ls -la /data/www/userbase/front | head
|
run: echo "deploy step finished; verify on host: /data/www/userbase/front"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user