chore: add ACS deployment check
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"test": "npm run test:node",
|
"test": "npm run test:node",
|
||||||
"test:node": "npm run build:server && node --test --test-isolation=none dist/server/**/*.test.js",
|
"test:node": "npm run build:server && node --test --test-isolation=none dist/server/**/*.test.js",
|
||||||
|
"deploy:check": "node scripts/deployment-check.mjs",
|
||||||
"db:check": "node scripts/check-db.mjs",
|
"db:check": "node scripts/check-db.mjs",
|
||||||
"minio:check": "node scripts/check-minio.mjs",
|
"minio:check": "node scripts/check-minio.mjs",
|
||||||
"dev": "tsx watch server/index.ts",
|
"dev": "tsx watch server/index.ts",
|
||||||
|
|||||||
186
scripts/deployment-check.mjs
Normal file
186
scripts/deployment-check.mjs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const DEFAULT_BASE = 'https://acs.apps.bokdev.in';
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
|
||||||
|
function argValue(name, fallback) {
|
||||||
|
const prefix = `--${name}=`;
|
||||||
|
const inline = process.argv.find((arg) => arg.startsWith(prefix));
|
||||||
|
if (inline) return inline.slice(prefix.length);
|
||||||
|
const index = process.argv.indexOf(`--${name}`);
|
||||||
|
if (index >= 0 && process.argv[index + 1]) return process.argv[index + 1];
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok(message) {
|
||||||
|
console.log(`[ok] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[fail] ${message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(message) {
|
||||||
|
console.warn(`[warn] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLocal(relativePath) {
|
||||||
|
return readFileSync(path.join(projectRoot, relativePath), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(args) {
|
||||||
|
return execFileSync('git', args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkJson(base, route) {
|
||||||
|
const url = new URL(route, base).toString();
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const body = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
fail(`${route} returned HTTP ${response.status}: ${body.slice(0, 160)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
if (parsed?.ok !== true) {
|
||||||
|
fail(`${route} response did not include ok=true: ${body.slice(0, 160)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ok(`${route} ${response.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`${route} check failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieHeaderFrom(response) {
|
||||||
|
if (typeof response.headers.getSetCookie === 'function') {
|
||||||
|
return response.headers.getSetCookie().map((cookie) => cookie.split(';')[0]).join('; ');
|
||||||
|
}
|
||||||
|
const cookie = response.headers.get('set-cookie');
|
||||||
|
return cookie ? cookie.split(',').map((part) => part.split(';')[0]).join('; ') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(base, username, password) {
|
||||||
|
if (!username || !password) return '';
|
||||||
|
|
||||||
|
const url = new URL('/api/auth/login', base).toString();
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
const body = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
fail(`/api/auth/login returned HTTP ${response.status}: ${body.slice(0, 160)}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
ok('/api/auth/login 200');
|
||||||
|
return cookieHeaderFrom(response);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`/api/auth/login check failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTemplate(base, cookie) {
|
||||||
|
const route = '/api/visit-requests/template';
|
||||||
|
const url = new URL(route, base).toString();
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, cookie ? { headers: { cookie } } : undefined);
|
||||||
|
const contentType = response.headers.get('content-type') ?? '';
|
||||||
|
const disposition = response.headers.get('content-disposition') ?? '';
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
fail(`${route} returned HTTP ${response.status}: ${body.slice(0, 200)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!contentType.includes('spreadsheet') && !disposition.includes('.xlsx')) {
|
||||||
|
fail(`${route} did not look like an xlsx download: content-type=${contentType || '-'}, content-disposition=${disposition || '-'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ok(`${route} xlsx download`);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`${route} check failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLocalTemplate() {
|
||||||
|
const templatePath = 'docs/form_sample.xlsx';
|
||||||
|
if (!existsSync(path.join(projectRoot, templatePath))) {
|
||||||
|
fail(`${templatePath} is missing locally`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ok(`${templatePath} exists locally`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
git(['ls-files', '--error-unmatch', templatePath]);
|
||||||
|
ok(`${templatePath} is tracked by git`);
|
||||||
|
} catch {
|
||||||
|
fail(`${templatePath} is not tracked by git`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDockerPackaging() {
|
||||||
|
const dockerignore = readLocal('.dockerignore');
|
||||||
|
const dockerfile = readLocal('Dockerfile');
|
||||||
|
|
||||||
|
if (!dockerignore.includes('!docs/form_sample.xlsx')) {
|
||||||
|
fail('.dockerignore does not allow docs/form_sample.xlsx into the build context');
|
||||||
|
} else {
|
||||||
|
ok('.dockerignore allows docs/form_sample.xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dockerfile.includes('COPY docs/form_sample.xlsx ./docs/form_sample.xlsx')) {
|
||||||
|
fail('Dockerfile does not copy docs/form_sample.xlsx into the runtime image');
|
||||||
|
} else {
|
||||||
|
ok('Dockerfile copies docs/form_sample.xlsx into the runtime image');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkGitState() {
|
||||||
|
try {
|
||||||
|
const head = git(['rev-parse', '--short', 'HEAD']);
|
||||||
|
ok(`local HEAD ${head}`);
|
||||||
|
|
||||||
|
const upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
||||||
|
const aheadBehind = git(['rev-list', '--left-right', '--count', `${upstream}...HEAD`]).split(/\s+/);
|
||||||
|
const behind = Number(aheadBehind[0] ?? 0);
|
||||||
|
const ahead = Number(aheadBehind[1] ?? 0);
|
||||||
|
if (ahead || behind) {
|
||||||
|
warn(`branch differs from ${upstream}: ahead ${ahead}, behind ${behind}`);
|
||||||
|
} else {
|
||||||
|
ok(`branch matches ${upstream}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
warn(`git state check skipped: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = argValue('base', DEFAULT_BASE).replace(/\/+$/, '/');
|
||||||
|
const username = argValue('username', process.env.ACS_CHECK_USER ?? '');
|
||||||
|
const password = argValue('password', process.env.ACS_CHECK_PASSWORD ?? '');
|
||||||
|
console.log(`[acs-deploy-check] base=${base}`);
|
||||||
|
|
||||||
|
checkLocalTemplate();
|
||||||
|
checkDockerPackaging();
|
||||||
|
checkGitState();
|
||||||
|
await checkJson(base, '/healthz');
|
||||||
|
await checkJson(base, '/db');
|
||||||
|
const cookie = await login(base, username, password);
|
||||||
|
await checkTemplate(base, cookie);
|
||||||
|
|
||||||
|
if (process.exitCode) {
|
||||||
|
console.error('[acs-deploy-check] failed');
|
||||||
|
process.exit(process.exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[acs-deploy-check] passed');
|
||||||
Reference in New Issue
Block a user