#!/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 use the Docker API to: // 1. tar the built assets straight out of THIS container's filesystem // (GET /containers/{jobId}/archive), and // 2. upload that tar into a throwaway helper container that has // /data/www/userbase mounted RW (PUT /containers/{helperId}/archive). // // 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_DIST = '/workspace/admin/userbase/front/dist/prod'; // 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 dockerJson(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(); }); } // Returns the raw tar response stream for a container path. function getArchive(id, path) { return new Promise((resolve, reject) => { const req = http.request( { socketPath: SOCKET, path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`, method: 'GET' }, (res) => resolve(res) ); req.on('error', reject); req.end(); }); } // Pipes a tar stream into a container path via PUT /archive. function putArchive(id, path, tarStream) { return new Promise((resolve, reject) => { const req = http.request( { socketPath: SOCKET, path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`, method: 'PUT', headers: { 'Content-Type': 'application/x-tar' }, }, (res) => { let buf = ''; res.on('data', (c) => (buf += c)); res.on('end', () => resolve({ status: res.statusCode, body: buf })); } ); req.on('error', reject); tarStream.pipe(req); }); } async function main() { const jobId = getContainerId(); console.log('job container id:', jobId); // 1) tar of the built assets, fetched from THIS container's own filesystem. const tar = await getArchive(jobId, WS_DIST); if (tar.statusCode !== 200) { let msg = ''; tar.on('data', (c) => (msg += c)); await new Promise((r) => tar.on('end', r)); throw new Error('get archive failed (status ' + tar.statusCode + '): ' + msg); } console.log('fetched dist tar (content-type:', tar.headers['content-type'], ')'); // 2) helper container with the host static dir mounted RW; keep it alive for the PUT. const createBody = { Image: HELPER_IMAGE, HostConfig: { NetworkMode: 'none', Binds: [`${HOST_STATIC}:${HOST_STATIC}`] }, Cmd: ['sh', '-c', `rm -rf ${HOST_STATIC}/front && mkdir -p ${HOST_STATIC}/front && exec tail -f /dev/null`], }; const created = await dockerJson('POST', '/containers/create', createBody); if (created.status >= 400) { throw new Error('create failed: ' + created.status + ' ' + created.body); } const helperId = JSON.parse(created.body).Id; console.log('helper container:', helperId); const started = await dockerJson('POST', `/containers/${helperId}/start`); if (started.status >= 400) { throw new Error('start failed: ' + started.status + ' ' + started.body); } // 3) upload the tar straight into the helper's mounted host dir. const put = await putArchive(helperId, `${HOST_STATIC}/front`, tar); console.log('put archive status:', put.status); if (put.status >= 400) { throw new Error('put archive failed: ' + put.status + ' ' + put.body); } // 4) cleanup helper. await dockerJson('DELETE', `/containers/${helperId}?force=true`); console.log('deploy OK ->', HOST_STATIC + '/front'); } main().then( () => process.exit(0), (err) => { console.error('DEPLOY FAILED:', err.message); process.exit(1); } );