feat(scripts): add deployment and build utility scripts

Replace old scripting approach with new build, deploy, push,
and uninstall utilities.
This commit is contained in:
ZhenYi 2026-05-11 17:08:29 +08:00
parent ac9ffb2a7a
commit 1daab11ba4
4 changed files with 306 additions and 0 deletions

86
scripts/build.js Normal file
View File

@ -0,0 +1,86 @@
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import process from 'process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
// colors
const G = (s) => `\x1b[0;32m${s}\x1b[0m`;
const Y = (s) => `\x1b[1;33m${s}\x1b[0m`;
const R = (s) => `\x1b[0;31m${s}\x1b[0m`;
const log = (msg) => console.log(`${G('[OK]')} ${msg}`);
const warn = (msg) => console.warn(`${Y('[WARN]')} ${msg}`);
const err = (msg) => { console.error(`${R('[ERR]')} ${msg}`); process.exit(1); };
const cmd = (...args) => execSync(args.join(' '), { cwd: ROOT, stdio: 'inherit' });
const exists = (c) => {
try { execSync(`which ${c}`, { stdio: 'pipe' }); return true; } catch { return false; }
};
// ── 1. Rust ──
if (exists('rustc')) {
log(`Rust ${execSync('rustc --version', { encoding: 'utf8' }).trim()}`);
} else {
warn('Rust not found, installing via rustup...');
execSync('curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y', { stdio: 'inherit' });
}
// ── 2. Node.js ──
if (exists('node')) {
log(`Node.js ${execSync('node --version', { encoding: 'utf8' }).trim()}`);
} else {
warn('Node.js not found, install manually or via nvm');
}
// ── 3. Bun ──
if (exists('bun')) {
log(`Bun ${execSync('bun --version', { encoding: 'utf8' }).trim()}`);
} else {
warn('Bun not found, installing...');
execSync('curl -fsSL https://bun.sh/install | bash', { stdio: 'inherit' });
}
// ── 4. Docker ──
if (exists('docker')) {
log(`Docker ${execSync('docker --version', { encoding: 'utf8' }).trim()}`);
} else {
warn('Docker not found, installing...');
execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit' });
}
// ── 5. Frontend build ──
log('Running bun install...');
cmd('bun', 'install');
log('Running bun run build...');
cmd('bun', 'run', 'build');
// ── 6. Rust release build ──
log('Running cargo build --release --workspace...');
cmd('cargo', 'build', '--release', '--workspace');
// ── 7. Docker images ──
const tag = execSync('git rev-parse --short HEAD', { encoding: 'utf8', cwd: ROOT }).trim();
log(`Building Docker images with tag: ${tag}`);
const images = [
['docker/app.Dockerfile', `app:${tag}`],
['docker/email.Dockerfile', `email-worker:${tag}`],
['docker/githook.Dockerfile', `git-hook:${tag}`],
['docker/gitserver.Dockerfile', `gitserver:${tag}`],
['docker/metrics.Dockerfile', `metrics-aggregator:${tag}`],
['docker/static.Dockerfile', `static-server:${tag}`],
['docker/gingress.Dockerfile', `gingress:${tag}`],
];
for (const [df, t] of images) {
log(`Building ${t}...`);
execSync(`docker build -f "${df}" -t "${t}" .`, { cwd: ROOT, stdio: 'inherit' });
}
log('All images built successfully.');

98
scripts/deploy.js Normal file
View File

@ -0,0 +1,98 @@
import { execSync } from 'child_process';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import process from 'process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
// colors
const G = (s) => `\x1b[0;32m${s}\x1b[0m`;
const Y = (s) => `\x1b[1;33m${s}\x1b[0m`;
const R = (s) => `\x1b[0;31m${s}\x1b[0m`;
const log = (msg) => console.log(`${G('[OK]')} ${msg}`);
const warn = (msg) => console.warn(`${Y('[WARN]')} ${msg}`);
const err = (msg) => { console.error(`${R('[ERR]')} ${msg}`); process.exit(1); };
// ── defaults ──
const NAMESPACE = process.env.NAMESPACE || 'app';
const RELEASE = process.env.RELEASE || 'deploy';
const CHART_DIR = process.env.CHART_DIR || resolve(ROOT, 'deploy');
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gtateam';
const TAG = process.env.TAG || execSync('git rev-parse --short HEAD', { encoding: 'utf8', cwd: ROOT }).trim();
const CONFIG_MAP = process.env.CONFIG_MAP || 'app-env';
const PVC_NAME = process.env.PVC_NAME || 'shared-data';
// ── prereqs ──
try { execSync('helm version --short', { stdio: 'pipe' }); } catch { err('helm not found'); }
try { execSync('kubectl version --client', { stdio: 'pipe' }); } catch { err('kubectl not found'); }
log(execSync('helm version --short', { encoding: 'utf8' }).trim());
log(execSync('kubectl version --client --short 2>/dev/null || kubectl version -o json 2>/dev/null | grep gitVersion', { encoding: 'utf8', shell: true }).trim());
// ── 1. Ensure namespace ──
log(`Ensuring namespace ${NAMESPACE} exists...`);
execSync(`kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -`, { stdio: 'inherit' });
// ── 2. Verify prerequisites ──
try { execSync(`kubectl get namespace "${NAMESPACE}"`, { stdio: 'pipe' }); }
catch { err(`Namespace '${NAMESPACE}' not found`); }
try { execSync(`kubectl get configmap "${CONFIG_MAP}" -n "${NAMESPACE}"`, { stdio: 'pipe' }); }
catch { err(`ConfigMap '${CONFIG_MAP}' not found in namespace '${NAMESPACE}'`); }
try { execSync(`kubectl get pvc "${PVC_NAME}" -n "${NAMESPACE}"`, { stdio: 'pipe' }); }
catch { err(`PVC '${PVC_NAME}' not found in namespace '${NAMESPACE}'`); }
// Protect from Helm deletion
execSync(`kubectl annotate configmap "${CONFIG_MAP}" -n "${NAMESPACE}" helm.sh/resource-policy=keep --overwrite`, { stdio: 'inherit' });
execSync(`kubectl annotate pvc "${PVC_NAME}" -n "${NAMESPACE}" helm.sh/resource-policy=keep --overwrite`, { stdio: 'inherit' });
// cert-manager ClusterIssuer
try { execSync('kubectl get clusterissuer cloudflare-acme-cluster-issuer', { stdio: 'pipe' }); }
catch { warn("ClusterIssuer 'cloudflare-acme-cluster-issuer' not found — TLS will fail"); }
log('Prerequisites verified');
// ── 3. Lint ──
log('Linting Helm chart...');
try { execSync(`helm lint "${CHART_DIR}"`, { stdio: 'inherit' }); }
catch { err('Helm lint failed'); }
// ── 4. Deploy ──
log(`Deploying release ${RELEASE} with tag ${TAG}...`);
try {
execSync(
`helm upgrade --install "${RELEASE}" "${CHART_DIR}"` +
` --namespace "${NAMESPACE}"` +
` --set imageRegistry="${REGISTRY}"` +
` --set imageTag="${TAG}"` +
` --set configMapName="${CONFIG_MAP}"` +
` --set pvcName="${PVC_NAME}"` +
` --timeout 5m`,
{ stdio: 'inherit' }
);
} catch {
err(`Deployment FAILED — release preserved for debugging.
Debug commands:
helm status ${RELEASE} -n ${NAMESPACE}
kubectl get pods -n ${NAMESPACE}
kubectl logs -n app <pod-name> --previous
helm rollback ${RELEASE} -n ${NAMESPACE} (rollback to previous release)
helm uninstall ${RELEASE} -n ${NAMESPACE} (remove failed release)`);
}
log(`Release ${RELEASE} deployed successfully`);
// ── 5. Verify ──
log('Checking deployment status...');
execSync(`kubectl get deployments -n "${NAMESPACE}" -l app.kubernetes.io/instance="${RELEASE}"`, { stdio: 'inherit' });
execSync(`kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/instance="${RELEASE}"`, { stdio: 'inherit' });
execSync(`kubectl get services -n "${NAMESPACE}" -l app.kubernetes.io/instance="${RELEASE}"`, { stdio: 'inherit' });
execSync(`kubectl get ingress -n "${NAMESPACE}"`, { stdio: 'inherit' });
log('Deployment complete');

42
scripts/push.js Normal file
View File

@ -0,0 +1,42 @@
import {execSync} from 'child_process';
import {dirname, resolve} from 'path';
import {fileURLToPath} from 'url';
import process from 'process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const G = (s) => `\x1b[0;32m${s}\x1b[0m`;
const R = (s) => `\x1b[0;31m${s}\x1b[0m`;
const log = (msg) => console.log(`${G('[OK]')} ${msg}`);
const err = (msg) => {
console.error(`${R('[ERR]')} ${msg}`);
process.exit(1);
};
const REGISTRY = process.env.REGISTRY || 'harbor.gitdata.me/gtateam';
const TAG = process.env.TAG || execSync('git rev-parse --short HEAD', {encoding: 'utf8', cwd: ROOT}).trim();
const USER = process.env.DOCKER_USERNAME;
const PASS = process.env.DOCKER_PASSWORD;
if (!USER) err('DOCKER_USERNAME env var required');
if (!PASS) err('DOCKER_PASSWORD env var required');
log(`Logging into ${REGISTRY}...`);
execSync(`echo "${PASS}" | docker login "${REGISTRY}" -u "${USER}" --password-stdin`, {stdio: 'inherit'});
const images = ['app', 'email-worker', 'git-hook', 'gitserver', 'metrics-aggregator', 'static-server', 'gingress'];
for (const name of images) {
const src = `${name}:${TAG}`;
const dst = `${REGISTRY}/${name}:${TAG}`;
log(`Tagging ${src} -> ${dst}`);
execSync(`docker tag "${src}" "${dst}"`, {stdio: 'inherit'});
log(`Pushing ${dst}`);
execSync(`docker push "${dst}"`, {stdio: 'inherit'});
}
log(`All images pushed to ${REGISTRY}`);

80
scripts/uninstall.js Normal file
View File

@ -0,0 +1,80 @@
import { execSync } from 'child_process';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import process from 'process';
import { createInterface } from 'readline';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
// colors
const G = (s) => `\x1b[0;32m${s}\x1b[0m`;
const Y = (s) => `\x1b[1;33m${s}\x1b[0m`;
const R = (s) => `\x1b[0;31m${s}\x1b[0m`;
const log = (msg) => console.log(`${G('[OK]')} ${msg}`);
const warn = (msg) => console.warn(`${Y('[WARN]')} ${msg}`);
const err = (msg) => console.error(`${R('[ERR]')} ${msg}`);
// ── defaults ──
const NAMESPACE = process.env.NAMESPACE || 'app';
const RELEASE = process.env.RELEASE || 'deploy';
const CONFIG_MAP = process.env.CONFIG_MAP || 'app-env';
const PVC_NAME = process.env.PVC_NAME || 'shared-data';
// ── confirm ──
function confirm(msg) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(msg, (answer) => { rl.close(); resolve(answer); });
});
}
console.log('');
warn(`This will remove Helm release '${RELEASE}' from namespace '${NAMESPACE}'.`);
warn('The following resources are PROTECTED and will NOT be deleted:');
warn(` - Namespace: ${NAMESPACE}`);
warn(` - ConfigMap: ${CONFIG_MAP}`);
warn(` - PVC: ${PVC_NAME}`);
console.log('');
const answer = await confirm('Continue? [y/N] ');
if (answer !== 'y' && answer !== 'Y') {
log('Cancelled');
process.exit(0);
}
// ── uninstall ──
log(`Uninstalling Helm release ${RELEASE}...`);
execSync(`helm uninstall "${RELEASE}" --namespace "${NAMESPACE}"`, { stdio: 'inherit' });
log('Helm release uninstalled');
// ── verify protected ──
function exists(cmd) {
try { execSync(cmd, { stdio: 'pipe' }); return true; } catch { return false; }
}
log('Verifying protected resources still exist...');
if (exists(`kubectl get namespace "${NAMESPACE}"`)) {
log(`Namespace '${NAMESPACE}' preserved`);
} else {
err(`Namespace '${NAMESPACE}' was deleted!`);
}
if (exists(`kubectl get configmap "${CONFIG_MAP}" -n "${NAMESPACE}"`)) {
log(`ConfigMap '${CONFIG_MAP}' preserved`);
} else {
err(`ConfigMap '${CONFIG_MAP}' was deleted!`);
}
if (exists(`kubectl get pvc "${PVC_NAME}" -n "${NAMESPACE}"`)) {
log(`PVC '${PVC_NAME}' preserved`);
} else {
err(`PVC '${PVC_NAME}' was deleted!`);
}
log(`Uninstall complete — remaining resources in namespace ${NAMESPACE}:`);
execSync(`kubectl get all,pvc,configmap,ingress -n "${NAMESPACE}"`, { stdio: 'inherit' });