#!/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); } );