Files
acs/scripts/deployment-check.mjs
2026-07-27 16:01:18 +09:00

187 lines
5.9 KiB
JavaScript

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');