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:
parent
ac9ffb2a7a
commit
1daab11ba4
86
scripts/build.js
Normal file
86
scripts/build.js
Normal 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
98
scripts/deploy.js
Normal 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
42
scripts/push.js
Normal 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
80
scripts/uninstall.js
Normal 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' });
|
||||
Loading…
Reference in New Issue
Block a user