setup/git.ts: - Add ensureRemoteBareRepo() — clones or updates a bare repo from a fixed remote URL, independent of GIT_DEFAULT_REPO_NAME/REMOTE_GIT_URL - Mirror codeberg.org/Clawdie/Clawdie-ISO into git jail on every setup --step git run so the agent always has latest ISO build scripts .agent/skills/build-iso/ (v0.0.1): - SKILL.md: full procedure — git clone from jail, build.sh, nginx publish, troubleshooting table, version history - scripts/build-iso.sh: clone from git jail → build.sh → publish to CMS jail nginx /downloads/ with dated archive + latest symlink; idempotent nginx location block injection + reload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: pass | Tests: FAIL — Tests 1 failed | 488 passed | 10 skipped (499)
260 lines
8.4 KiB
TypeScript
260 lines
8.4 KiB
TypeScript
/**
|
|
* Step: git — Provision the default local git storage jail.
|
|
*
|
|
* Creates a persistent git jail, installs a minimal package baseline, creates
|
|
* /srv/git, and mirrors the current repository into a bare repo by default.
|
|
*/
|
|
import { execSync } from 'child_process';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
CODE_HOSTING_MODE,
|
|
GIT_DEFAULT_REPO_NAME,
|
|
GIT_STORAGE_ROOT,
|
|
REMOTE_GIT_URL,
|
|
} from '../src/config.js';
|
|
import { getGitBastillePlan } from '../src/jail-config.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { syncLocalHosts } from './hosts.js';
|
|
import { loadPackageList, mountPkgCacheInJail } from './packages.js';
|
|
import { ensureEnvFile, writeEnvLine } from './profile.js';
|
|
import { commandExists, getPlatform } from './platform.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
const BASTILLE_ROOT = '/usr/local/bastille';
|
|
const BASTILLE_RELEASES = path.join(BASTILLE_ROOT, 'releases');
|
|
const GIT_PACKAGES = loadPackageList('git-jail.txt');
|
|
|
|
function runCommand(cmd: string, opts: { inherit?: boolean } = {}): string {
|
|
const output = execSync(cmd, {
|
|
encoding: 'utf-8',
|
|
stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
return typeof output === 'string' ? output.trim() : '';
|
|
}
|
|
|
|
function jailRoot(jailName: string): string {
|
|
return path.join(BASTILLE_ROOT, 'jails', jailName, 'root');
|
|
}
|
|
|
|
function jexec(jailName: string, cmd: string): string {
|
|
return runCommand(`jexec ${jailName} /bin/sh -lc ${JSON.stringify(cmd)}`);
|
|
}
|
|
|
|
function jailExists(jailName: string): boolean {
|
|
return fs.existsSync(path.join(BASTILLE_ROOT, 'jails', jailName));
|
|
}
|
|
|
|
function jailRunning(jailName: string): boolean {
|
|
try {
|
|
const output = runCommand('jls -N name');
|
|
return output.split('\n').map((line) => line.trim()).includes(jailName);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function hasReleaseBootstrap(release: string): boolean {
|
|
return fs.existsSync(path.join(BASTILLE_RELEASES, release));
|
|
}
|
|
|
|
function bridgeHasGateway(bridge: string, gateway: string): boolean {
|
|
try {
|
|
return runCommand(`ifconfig ${bridge}`).includes(`inet ${gateway} `);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function failSetup(error: string): never {
|
|
emitStatus('SETUP_GIT', {
|
|
STATUS: 'failed',
|
|
ERROR: error,
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
function createGitJail(): void {
|
|
const plan = getGitBastillePlan();
|
|
logger.info({ jailName: plan.jailName, jailIp: plan.jailIp }, 'Creating git jail');
|
|
runCommand(`bastille ${plan.createArgs.join(' ')}`, { inherit: true });
|
|
runCommand(`bastille config ${plan.jailName} set host.hostname ${plan.hostname}`, {
|
|
inherit: true,
|
|
});
|
|
runCommand(`bastille restart ${plan.jailName}`, { inherit: true });
|
|
}
|
|
|
|
function ensureGitPackages(jailName: string): void {
|
|
runCommand(`bastille pkg ${jailName} install -y ${GIT_PACKAGES.join(' ')}`, {
|
|
inherit: true,
|
|
});
|
|
}
|
|
|
|
function ensureGitStorage(jailName: string): void {
|
|
jexec(jailName, `install -d -m 0755 ${GIT_STORAGE_ROOT}`);
|
|
}
|
|
|
|
function repoHostPath(jailName: string, repoName: string): string {
|
|
return path.join(jailRoot(jailName), GIT_STORAGE_ROOT.replace(/^\/+/, ''), repoName);
|
|
}
|
|
|
|
function repoExists(jailName: string, repoName: string): boolean {
|
|
return fs.existsSync(repoHostPath(jailName, repoName));
|
|
}
|
|
|
|
function currentProjectIsGitRepo(projectRoot: string): boolean {
|
|
return fs.existsSync(path.join(projectRoot, '.git'));
|
|
}
|
|
|
|
function ensureBareRepo(jailName: string, projectRoot: string, repoName: string): string {
|
|
const target = repoHostPath(jailName, repoName);
|
|
|
|
if (!repoExists(jailName, repoName)) {
|
|
jexec(jailName, `git init --bare ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`);
|
|
}
|
|
|
|
if (currentProjectIsGitRepo(projectRoot)) {
|
|
runCommand(`git -C ${JSON.stringify(projectRoot)} push --mirror file://${target}`, {
|
|
inherit: true,
|
|
});
|
|
} else if (REMOTE_GIT_URL) {
|
|
jexec(
|
|
jailName,
|
|
[
|
|
`rm -rf ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`,
|
|
`git clone --mirror ${JSON.stringify(REMOTE_GIT_URL)} ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`,
|
|
].join(' && '),
|
|
);
|
|
}
|
|
|
|
if (REMOTE_GIT_URL) {
|
|
jexec(
|
|
jailName,
|
|
[
|
|
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} remote remove origin >/dev/null 2>&1 || true`,
|
|
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} remote add origin ${JSON.stringify(REMOTE_GIT_URL)}`,
|
|
].join(' && '),
|
|
);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
function ensureRemoteBareRepo(jailName: string, remoteUrl: string, repoName: string): string {
|
|
const target = repoHostPath(jailName, repoName);
|
|
const repoPath = path.posix.join(GIT_STORAGE_ROOT, repoName);
|
|
|
|
if (!repoExists(jailName, repoName)) {
|
|
jexec(jailName, `git clone --mirror ${JSON.stringify(remoteUrl)} ${repoPath}`);
|
|
} else {
|
|
jexec(jailName, `git --git-dir ${repoPath} remote update`);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
function verifyGitRepo(jailName: string, repoName: string): void {
|
|
jexec(
|
|
jailName,
|
|
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} rev-parse --is-bare-repository`,
|
|
);
|
|
}
|
|
|
|
function writeGitEnv(projectRoot: string, jailName: string, jailIp: string): void {
|
|
const envFile = ensureEnvFile(projectRoot);
|
|
writeEnvLine(envFile, 'FEATURE_GIT', 'YES');
|
|
if (CODE_HOSTING_MODE !== 'gitea') {
|
|
writeEnvLine(envFile, 'CODE_HOSTING_MODE', 'git');
|
|
writeEnvLine(envFile, 'FEATURE_GITEA', 'NO');
|
|
}
|
|
writeEnvLine(envFile, 'WARDEN_GIT_IP', jailIp);
|
|
writeEnvLine(envFile, 'GIT_JAIL_NAME', jailName);
|
|
writeEnvLine(envFile, 'GIT_JAIL_IP', jailIp);
|
|
writeEnvLine(envFile, 'GIT_STORAGE_ROOT', GIT_STORAGE_ROOT);
|
|
writeEnvLine(envFile, 'GIT_DEFAULT_REPO_NAME', GIT_DEFAULT_REPO_NAME);
|
|
if (REMOTE_GIT_URL) {
|
|
writeEnvLine(envFile, 'REMOTE_GIT_URL', REMOTE_GIT_URL);
|
|
}
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
const platform = getPlatform();
|
|
const projectRoot = process.cwd();
|
|
const plan = getGitBastillePlan();
|
|
|
|
if (platform !== 'freebsd') failSetup('unsupported_platform');
|
|
if (!commandExists('bastille')) failSetup('bastille_not_installed');
|
|
if (!commandExists('jexec')) failSetup('missing_jexec');
|
|
if (!commandExists('jls')) failSetup('missing_jls');
|
|
if (!commandExists('ifconfig')) failSetup('missing_ifconfig');
|
|
if (!hasReleaseBootstrap(plan.release)) failSetup('release_not_bootstrapped');
|
|
if (!bridgeHasGateway(plan.bridge, plan.gateway)) {
|
|
failSetup(`bridge_not_ready_${plan.bridge}_${plan.gateway}`);
|
|
}
|
|
|
|
logger.info(
|
|
{
|
|
jailName: plan.jailName,
|
|
jailIp: plan.jailIp,
|
|
repoName: GIT_DEFAULT_REPO_NAME,
|
|
storageRoot: GIT_STORAGE_ROOT,
|
|
},
|
|
'Starting git setup',
|
|
);
|
|
|
|
if (!jailExists(plan.jailName)) {
|
|
createGitJail();
|
|
} else if (!jailRunning(plan.jailName)) {
|
|
runCommand(`bastille start ${plan.jailName}`, { inherit: true });
|
|
}
|
|
|
|
runCommand(`bastille config ${plan.jailName} set host.hostname ${plan.hostname}`, {
|
|
inherit: true,
|
|
});
|
|
runCommand(`bastille restart ${plan.jailName}`, { inherit: true });
|
|
mountPkgCacheInJail(plan.jailName);
|
|
|
|
ensureGitPackages(plan.jailName);
|
|
ensureGitStorage(plan.jailName);
|
|
const repoPath = ensureBareRepo(plan.jailName, projectRoot, GIT_DEFAULT_REPO_NAME);
|
|
verifyGitRepo(plan.jailName, GIT_DEFAULT_REPO_NAME);
|
|
|
|
// Mirror clawdie-iso build scripts into the git jail so the agent can rebuild
|
|
// the USB installer from latest commits without leaving the host.
|
|
const ISO_REMOTE = 'https://codeberg.org/Clawdie/Clawdie-ISO.git';
|
|
const ISO_REPO_NAME = 'clawdie-iso';
|
|
ensureRemoteBareRepo(plan.jailName, ISO_REMOTE, ISO_REPO_NAME);
|
|
verifyGitRepo(plan.jailName, ISO_REPO_NAME);
|
|
|
|
writeGitEnv(projectRoot, plan.jailName, plan.jailIp);
|
|
syncLocalHosts();
|
|
|
|
emitStatus('SETUP_GIT', {
|
|
GIT_JAIL: plan.jailName,
|
|
GIT_IP: plan.jailIp,
|
|
CODE_HOSTING_MODE: CODE_HOSTING_MODE === 'gitea' ? 'gitea' : 'git',
|
|
REMOTE_GIT_URL: REMOTE_GIT_URL || 'none',
|
|
STORAGE_ROOT: GIT_STORAGE_ROOT,
|
|
DEFAULT_REPO: GIT_DEFAULT_REPO_NAME,
|
|
REPO_PATH: repoPath,
|
|
FEATURE_GIT: true,
|
|
STATUS: 'success',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
|
|
console.log('');
|
|
console.log('─'.repeat(52));
|
|
console.log(` git jail ready at ${plan.jailIp}`);
|
|
console.log(` Jail name : ${plan.jailName}`);
|
|
console.log(` Storage : ${GIT_STORAGE_ROOT}`);
|
|
console.log(` Repo : ${GIT_DEFAULT_REPO_NAME}`);
|
|
console.log('');
|
|
console.log(' Local mirror path on host:');
|
|
console.log(` ${repoPath}`);
|
|
console.log('');
|
|
console.log(` ISO repo : ${ISO_REPO_NAME}`);
|
|
console.log('─'.repeat(52));
|
|
console.log('');
|
|
}
|