141 lines
4.9 KiB
JavaScript
141 lines
4.9 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 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);
|
|
}
|
|
);
|