Files
mward4 a6f8e67976 VShell 5.0.1-3985 container image
Build the image from the vendor vshell-https .deb on ubuntu:24.04 (required:
GLIBC_2.38 / GLIBCXX_3.4.32). Contains the Dockerfile, the entrypoint that
starts and supervises the three VShell daemons without systemd, the
no-container-runtime build/validate/push tooling used to publish the image, and
the vendored .deb (sha256 pinned).

Published as gitea.milesward.dev/mward4/vshell:5.0.1-3985 and :latest.
Manifest digest sha256:8b0ff00be981fa05f1e4a4e4ff5307b6c7e5e61b487d4fbe7e02fefb2a04659f.
2026-10-05 12:15:20 -04:00

82 lines
3.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Push an image built by tools/build-image.mjs to a Docker Registry v2 endpoint
// (this deployment uses the Gitea container registry).
//
// Usage:
// node tools/push-image.mjs --summary dist/image-summary.json \
// --registry https://gitea.milesward.dev --repo mward4/vshell \
// --user mward4 --token <token>
//
// Credentials may also come from GITEA_USER / GITEA_TOKEN.
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const args = {};
for (let i = 2; i < process.argv.length; i += 2) {
if (!process.argv[i].startsWith('--')) throw new Error(`unexpected argument ${process.argv[i]}`);
args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
}
const SUMMARY = path.resolve(args.summary || 'dist/image-summary.json');
const REGISTRY = (args.registry || 'https://gitea.milesward.dev').replace(/\/+$/, '');
const REPO = args.repo || 'mward4/vshell';
const USER = args.user || process.env.GITEA_USER || 'mward4';
const TOKEN = args.token || process.env.GITEA_TOKEN;
if (!TOKEN) throw new Error('missing --token / GITEA_TOKEN');
const AUTH = { Authorization: 'Basic ' + Buffer.from(`${USER}:${TOKEN}`).toString('base64') };
const summary = JSON.parse(await readFile(SUMMARY, 'utf8'));
const manifestBuf = await readFile(summary.manifest.blob);
const manifest = JSON.parse(manifestBuf);
// Every blob the manifest references must exist in the target repository.
const blobs = [
{ digest: summary.base.configDigest, file: summary.base.configBlob },
{ digest: summary.base.digest, file: summary.base.blob },
{ digest: summary.config.digest, file: summary.config.blob },
{ digest: summary.layer.digest, file: summary.layer.blob },
];
for (const b of blobs) {
const data = await readFile(b.file);
const head = await fetch(`${REGISTRY}/v2/${REPO}/blobs/${b.digest}`, { method: 'HEAD', headers: AUTH });
if (head.status === 200) {
console.log(`exists ${b.digest} (${(data.length / 1048576).toFixed(1)} MiB)`);
continue;
}
const start = await fetch(`${REGISTRY}/v2/${REPO}/blobs/uploads/`, { method: 'POST', headers: AUTH });
if (start.status !== 202) throw new Error(`upload start failed: ${start.status} ${await start.text()}`);
const location = new URL(start.headers.get('location'), REGISTRY);
location.searchParams.set('digest', b.digest);
const put = await fetch(location, {
method: 'PUT',
headers: { ...AUTH, 'Content-Type': 'application/octet-stream', 'Content-Length': String(data.length) },
body: data,
});
if (put.status !== 201) throw new Error(`blob upload failed: ${put.status} ${await put.text()}`);
console.log(`uploaded ${b.digest} (${(data.length / 1048576).toFixed(1)} MiB)`);
}
for (const tag of summary.tags) {
const ref = tag.includes(':') ? tag.split(':')[1] : tag;
const put = await fetch(`${REGISTRY}/v2/${REPO}/manifests/${ref}`, {
method: 'PUT',
headers: { ...AUTH, 'Content-Type': manifest.mediaType, 'Content-Length': String(manifestBuf.length) },
body: manifestBuf,
});
if (!put.ok) throw new Error(`manifest push failed: ${put.status} ${await put.text()}`);
console.log(`pushed ${REPO}:${ref} -> ${put.headers.get('docker-content-digest')}`);
}
// Read it back so there is independent evidence of what landed.
for (const tag of summary.tags) {
const ref = tag.includes(':') ? tag.split(':')[1] : tag;
const check = await fetch(`${REGISTRY}/v2/${REPO}/manifests/${ref}`, { headers: { ...AUTH, Accept: manifest.mediaType } });
if (!check.ok) throw new Error(`pull-back failed for ${ref}: ${check.status}`);
const got = check.headers.get('docker-content-digest');
const body = await check.json();
const ok = got === summary.manifest.digest && JSON.stringify(body) === JSON.stringify(manifest);
console.log(`verified ${REPO}:${ref} -> ${got} ${ok ? 'OK' : 'MISMATCH (expected ' + summary.manifest.digest + ')'}`);
if (!ok) process.exitCode = 1;
}