Files
userbase/scripts/deploy-via-docker.js
T

143 lines
5.1 KiB
JavaScript

#!/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)
const WS_MOUNT = '/workspace/admin/userbase';
// The job container's ID, derived from its cgroup (robust regardless of hostname).
function getContainerId() {
const cg = fs.readFileSync('/proc/self/cgroup', 'utf8');
const m = cg.match(/[0-9a-f]{64}/);
if (!m) throw new Error('cannot determine container id from cgroup');
return m[0];
}
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 mount by inspecting THIS container
// via the Docker daemon (docker.sock). This is far more reliable than parsing
// /proc/self/mountinfo (which reports the underlying block device, not the bind
// source). The job container's hostname is its short ID.
async function findWorkspaceSource() {
const id = getContainerId();
const r = await dockerReq('GET', `/containers/${id}/json`);
if (r.status >= 400) {
throw new Error('inspect self failed: ' + r.status + ' ' + r.body);
}
const info = JSON.parse(r.body);
const mounts = info.Mounts || [];
for (const m of mounts) {
if (m.Destination === WS_MOUNT) {
return m.Source; // host path of the docker volume _data dir
}
}
throw new Error('workspace mount ' + WS_MOUNT + ' not found in container inspect');
}
async function main() {
const wsSource = await findWorkspaceSource();
console.log('workspace source (host path):', wsSource);
// Inside the helper, the workspace is bound at /src (see Binds below).
const SRC_IN_HELPER = '/src/front/dist/prod';
const createBody = {
Image: HELPER_IMAGE,
Tty: true,
HostConfig: {
NetworkMode: 'none',
Binds: [`${wsSource}:/src:ro`, `${HOST_STATIC}:${HOST_STATIC}`],
},
Cmd: [
'sh',
'-c',
// Write diagnostics to the host-mounted dir so we can inspect after the run.
`DBG=${HOST_STATIC}/_debug.txt; ` +
`echo "wsSource=${wsSource}" > $DBG; ` +
`echo "--- ls /src/front/dist/prod ---" >> $DBG; ` +
`ls -la ${SRC_IN_HELPER} >> $DBG 2>&1; ` +
`echo "src file count: $(find ${SRC_IN_HELPER} -type f 2>/dev/null | wc -l)" >> $DBG; ` +
`set -e; ` +
`rm -rf ${HOST_STATIC}/front && mkdir -p ${HOST_STATIC}/front && ` +
`cp -r ${SRC_IN_HELPER}/. ${HOST_STATIC}/front/ 2>> $DBG; ` +
`echo "dest file count: $(find ${HOST_STATIC}/front -type f 2>/dev/null | wc -l)" >> $DBG; ` +
`echo "copied"`,
],
};
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);
}
);