From 1daab11ba456a6789ec458a4386297002b615dad Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Mon, 11 May 2026 17:08:29 +0800 Subject: [PATCH] feat(scripts): add deployment and build utility scripts Replace old scripting approach with new build, deploy, push, and uninstall utilities. --- scripts/build.js | 86 ++++++++++++++++++++++++++++++++++++++ scripts/deploy.js | 98 ++++++++++++++++++++++++++++++++++++++++++++ scripts/push.js | 42 +++++++++++++++++++ scripts/uninstall.js | 80 ++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 scripts/build.js create mode 100644 scripts/deploy.js create mode 100644 scripts/push.js create mode 100644 scripts/uninstall.js diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..9a5a7ec --- /dev/null +++ b/scripts/build.js @@ -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.'); diff --git a/scripts/deploy.js b/scripts/deploy.js new file mode 100644 index 0000000..61322cd --- /dev/null +++ b/scripts/deploy.js @@ -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 --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'); diff --git a/scripts/push.js b/scripts/push.js new file mode 100644 index 0000000..5ebec81 --- /dev/null +++ b/scripts/push.js @@ -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}`); diff --git a/scripts/uninstall.js b/scripts/uninstall.js new file mode 100644 index 0000000..20ad9eb --- /dev/null +++ b/scripts/uninstall.js @@ -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' });