chore: add ACS deployment check
This commit is contained in:
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