From dabb053b1970a025917fdfd21acc993004fe37e5 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 4 Aug 2026 22:58:33 +0800 Subject: [PATCH] fix(deploy): use Docker archive API to copy dist (no volume-source discovery) --- scripts/deploy-via-docker.js | 132 +++++++++++++++++------------------ 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/scripts/deploy-via-docker.js b/scripts/deploy-via-docker.js index b6de5c4..baf8093 100644 --- a/scripts/deploy-via-docker.js +++ b/scripts/deploy-via-docker.js @@ -6,9 +6,11 @@ // 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. +// 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. @@ -19,7 +21,7 @@ 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'; +const WS_DIST = '/workspace/admin/userbase/front/dist/prod'; // The job container's ID, derived from its cgroup (robust regardless of hostname). function getContainerId() { @@ -29,7 +31,7 @@ function getContainerId() { return m[0]; } -function dockerReq(method, path, bodyObj) { +function dockerJson(method, path, bodyObj) { return new Promise((resolve, reject) => { const data = bodyObj ? JSON.stringify(bodyObj) : null; const headers = {}; @@ -51,85 +53,81 @@ function dockerReq(method, path, bodyObj) { }); } -// 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'); +// 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 wsSource = await findWorkspaceSource(); - console.log('workspace source (host path):', wsSource); + const jobId = getContainerId(); + console.log('job container id:', jobId); - // Inside the helper, the workspace is bound at /src (see Binds below). - const SRC_IN_HELPER = '/src/front/dist/prod'; + // 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, - 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"`, - ], + 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 dockerReq('POST', '/containers/create', createBody); + const created = await dockerJson('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 helperId = JSON.parse(created.body).Id; + console.log('helper container:', helperId); - const started = await dockerReq('POST', `/containers/${id}/start`); + const started = await dockerJson('POST', `/containers/${helperId}/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); + // 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'); }