fix(deploy): correct dist nesting + capture host-readable diagnostics
deploy-userbase-front / build-and-deploy (push) Successful in 29s
deploy-userbase-front / build-and-deploy (push) Successful in 29s
This commit is contained in:
@@ -52,4 +52,4 @@ jobs:
|
||||
run: node scripts/deploy-via-docker.js
|
||||
|
||||
- name: Done
|
||||
run: 'echo "deploy step finished; verify on host: /data/www/userbase/front"'
|
||||
run: 'echo "deploy step finished; inspect host /data/www/userbase/_debug.txt for the full report"'
|
||||
|
||||
+165
-53
@@ -6,32 +6,31 @@
|
||||
// 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:
|
||||
// mounted by default, so we use the Docker HTTP API to:
|
||||
// 1. tar the built assets straight out of THIS container's filesystem
|
||||
// (GET /containers/{jobId}/archive), and
|
||||
// (GET /containers/{jobId}/archive?path=.../), and
|
||||
// 2. upload that tar into a throwaway helper container that has
|
||||
// /data/www/userbase mounted RW (PUT /containers/{helperId}/archive).
|
||||
//
|
||||
// Diagnostics: every step is also written to /data/www/userbase/_debug.txt on
|
||||
// the HOST via `docker exec` into the helper (the job container itself cannot
|
||||
// write there). The Gitea logs API is unavailable here, so this file is our
|
||||
// only reliable window into what happened.
|
||||
//
|
||||
// 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 path = require('path');
|
||||
|
||||
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];
|
||||
}
|
||||
// --- low level docker http helpers -------------------------------------------
|
||||
|
||||
function dockerJson(method, path, bodyObj) {
|
||||
function dockerJson(method, p, bodyObj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = bodyObj ? JSON.stringify(bodyObj) : null;
|
||||
const headers = {};
|
||||
@@ -40,7 +39,7 @@ function dockerJson(method, path, bodyObj) {
|
||||
headers['Content-Length'] = Buffer.byteLength(data);
|
||||
}
|
||||
const req = http.request(
|
||||
{ socketPath: SOCKET, path, method, headers },
|
||||
{ socketPath: SOCKET, path: p, method, headers },
|
||||
(res) => {
|
||||
let buf = '';
|
||||
res.on('data', (c) => (buf += c));
|
||||
@@ -53,11 +52,14 @@ function dockerJson(method, path, bodyObj) {
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the raw tar response stream for a container path.
|
||||
function getArchive(id, path) {
|
||||
function getArchive(id, p) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{ socketPath: SOCKET, path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`, method: 'GET' },
|
||||
{
|
||||
socketPath: SOCKET,
|
||||
path: `/containers/${id}/archive?path=${encodeURIComponent(p)}`,
|
||||
method: 'GET',
|
||||
},
|
||||
(res) => resolve(res)
|
||||
);
|
||||
req.on('error', reject);
|
||||
@@ -65,13 +67,12 @@ function getArchive(id, path) {
|
||||
});
|
||||
}
|
||||
|
||||
// Pipes a tar stream into a container path via PUT /archive.
|
||||
function putArchive(id, path, tarStream) {
|
||||
function putArchive(id, p, tarStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
socketPath: SOCKET,
|
||||
path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`,
|
||||
path: `/containers/${id}/archive?path=${encodeURIComponent(p)}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/x-tar' },
|
||||
},
|
||||
@@ -86,55 +87,166 @@ function putArchive(id, path, tarStream) {
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const jobId = getContainerId();
|
||||
console.log('job container id:', jobId);
|
||||
// --- helper container + diagnostics ------------------------------------------
|
||||
|
||||
// 1) tar of the built assets, fetched from THIS container's own filesystem.
|
||||
const tar = await getArchive(jobId, WS_DIST);
|
||||
// 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) {
|
||||
// fall back to hostname prefix if cgroup has no full id
|
||||
const h = fs.readFileSync('/etc/hostname', 'utf8').trim();
|
||||
return h;
|
||||
}
|
||||
return m[0];
|
||||
}
|
||||
|
||||
let HELPER_ID = null;
|
||||
let DEBUG_BUF = [];
|
||||
|
||||
// Append a line to the in-memory debug buffer; flushes to host at key points.
|
||||
function dbg(line) {
|
||||
const ts = new Date().toISOString();
|
||||
DEBUG_BUF.push(`[${ts}] ${line}`);
|
||||
}
|
||||
|
||||
// Flush the buffered debug lines into the HOST file via docker exec on helper.
|
||||
async function flushDebug() {
|
||||
if (!HELPER_ID || DEBUG_BUF.length === 0) return;
|
||||
const msg = DEBUG_BUF.join('\n') + '\n';
|
||||
DEBUG_BUF = [];
|
||||
const created = await dockerJson('POST', `/containers/${HELPER_ID}/exec`, {
|
||||
Cmd: ['sh', '-c', 'printf "%s" "$MSG" >> /data/www/userbase/_debug.txt'],
|
||||
Env: ['MSG=' + msg],
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
});
|
||||
if (created.status >= 400) {
|
||||
console.error('exec create for debug failed:', created.status, created.body);
|
||||
return;
|
||||
}
|
||||
const execId = JSON.parse(created.body).Id;
|
||||
const started = await dockerJson('POST', `/exec/${execId}/start`, {
|
||||
Detach: true,
|
||||
Tty: false,
|
||||
});
|
||||
if (started.status >= 400) {
|
||||
console.error('exec start for debug failed:', started.status, started.body);
|
||||
}
|
||||
// give the exec a moment to flush before any subsequent cleanup
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// --- main --------------------------------------------------------------------
|
||||
|
||||
function resolveDistPath() {
|
||||
const candidates = [
|
||||
path.join(process.cwd(), 'front', 'dist', 'prod'),
|
||||
'/workspace/admin/userbase/front/dist/prod',
|
||||
'/workspace/admin/userbase/front/dist/prod',
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (fs.existsSync(c)) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
dbg('START script version=2026-08-04b');
|
||||
dbg('cwd=' + process.cwd());
|
||||
|
||||
// 1) helper container with the host static dir mounted RW; keep it alive.
|
||||
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 helper failed: ' + created.status + ' ' + created.body);
|
||||
}
|
||||
HELPER_ID = JSON.parse(created.body).Id;
|
||||
dbg('helper created id=' + HELPER_ID);
|
||||
|
||||
const started = await dockerJson('POST', `/containers/${HELPER_ID}/start`);
|
||||
if (started.status >= 400) {
|
||||
throw new Error('start helper failed: ' + started.status + ' ' + started.body);
|
||||
}
|
||||
dbg('helper started');
|
||||
|
||||
// 2) locate the built assets inside THIS container.
|
||||
const jobId = getContainerId();
|
||||
dbg('job container id=' + jobId);
|
||||
|
||||
const distDir = resolveDistPath();
|
||||
if (!distDir) {
|
||||
dbg('dist dir NOT found; cwd listing:');
|
||||
try {
|
||||
dbg(fs.readdirSync(process.cwd()).join(' '));
|
||||
const front = path.join(process.cwd(), 'front');
|
||||
if (fs.existsSync(front)) dbg('front listing: ' + fs.readdirSync(front).join(' '));
|
||||
} catch (e) {
|
||||
dbg('ls err: ' + e.message);
|
||||
}
|
||||
throw new Error('built dist/prod not found under any candidate path');
|
||||
}
|
||||
dbg('dist dir=' + distDir);
|
||||
dbg('dist listing: ' + fs.readdirSync(distDir).join(' '));
|
||||
|
||||
// 3) tar of the built assets, fetched from THIS container's own filesystem.
|
||||
// Trailing slash => tar contains the directory CONTENTS at the top level,
|
||||
// so they land directly under /data/www/userbase/front (not under prod/).
|
||||
const tar = await getArchive(jobId, distDir + '/');
|
||||
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'], ')');
|
||||
dbg('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);
|
||||
// 4) upload the tar straight into the helper's mounted host dir.
|
||||
const put = await putArchive(HELPER_ID, `${HOST_STATIC}/front`, tar);
|
||||
dbg('put archive status=' + put.status + (put.status >= 400 ? ' ' + put.body : ''));
|
||||
if (put.status >= 400) {
|
||||
throw new Error('put archive failed: ' + put.status + ' ' + put.body);
|
||||
}
|
||||
|
||||
// 4) cleanup helper.
|
||||
await dockerJson('DELETE', `/containers/${helperId}?force=true`);
|
||||
// 5) verify on host via exec into helper.
|
||||
const verifyExec = await dockerJson('POST', `/containers/${HELPER_ID}/exec`, {
|
||||
Cmd: [
|
||||
'sh',
|
||||
'-c',
|
||||
`echo "count=$(find ${HOST_STATIC}/front -type f | wc -l)"; ls -la ${HOST_STATIC}/front | head -20`,
|
||||
],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
const vExecId = JSON.parse(verifyExec.body).Id;
|
||||
const vStart = await dockerJson('POST', `/exec/${vExecId}/start`, {
|
||||
Detach: false,
|
||||
Tty: false,
|
||||
});
|
||||
dbg('verify output: ' + vStart.body.replace(/\n/g, ' | '));
|
||||
|
||||
console.log('deploy OK ->', HOST_STATIC + '/front');
|
||||
dbg('DEPLOY OK -> ' + HOST_STATIC + '/front');
|
||||
}
|
||||
|
||||
main().then(
|
||||
() => process.exit(0),
|
||||
(err) => {
|
||||
main()
|
||||
.then(async () => {
|
||||
await flushDebug();
|
||||
if (HELPER_ID) await dockerJson('DELETE', `/containers/${HELPER_ID}?force=true`);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(async (err) => {
|
||||
dbg('DEPLOY FAILED: ' + err.stack);
|
||||
await flushDebug();
|
||||
if (HELPER_ID) await dockerJson('DELETE', `/containers/${HELPER_ID}?force=true`);
|
||||
console.error('DEPLOY FAILED:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user