feat(devops): admin-only collector helpers, public version, deps + tests
Bumps @bytelyst/devops to 0.1.2. Adds: - getBuildInfo() — public-safe build info (commit + branch + image), no env vars or runtime introspection. Suitable for unauthenticated /api/devops/version endpoints used by ops/CI. - getRuntimeInfo() / getConfigInfo() — exposed individually for callers who want to compose their own payload. - readServiceVersion(import.meta.url) — walks up to package.json so consumers don't need to hardcode the version string. - dependencyCheck(name, fn, timeoutMs) — timed health check wrapper with consistent ok/latency/detail shape. Enforces a hard timeout. - httpDependencyCheck(name, url, timeoutMs) — convenience for HTTP probes. Other improvements: - SECRET_PATTERN extracted as a module constant. - 17 unit tests covering build/runtime/config collectors, secret-leak guards, version walker fallbacks, dep timeouts, full collect payload. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
17780adc1a
commit
51fc8d09b0
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bytelyst/devops",
|
"name": "@bytelyst/devops",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Runtime devops metadata (build info, uptime, health) — server collector + React UI",
|
"description": "Runtime devops metadata (build info, uptime, health) — server collector + React UI",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
212
packages/devops/src/__tests__/server.test.ts
Normal file
212
packages/devops/src/__tests__/server.test.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Tests for @bytelyst/devops/server.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
collectDevopsInfo,
|
||||||
|
dependencyCheck,
|
||||||
|
getBuildInfo,
|
||||||
|
getConfigInfo,
|
||||||
|
getRuntimeInfo,
|
||||||
|
readServiceVersion,
|
||||||
|
} from '../server.js';
|
||||||
|
|
||||||
|
describe('@bytelyst/devops/server', () => {
|
||||||
|
let envBackup: Record<string, string | undefined>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
envBackup = { ...process.env };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const k of Object.keys(process.env)) {
|
||||||
|
if (!(k in envBackup)) delete process.env[k];
|
||||||
|
}
|
||||||
|
Object.assign(process.env, envBackup);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBuildInfo', () => {
|
||||||
|
it('reads BYTELYST_* env vars', () => {
|
||||||
|
process.env.BYTELYST_COMMIT_SHA = 'abcdef0';
|
||||||
|
process.env.BYTELYST_COMMIT_SHA_FULL = 'abcdef0123456789';
|
||||||
|
process.env.BYTELYST_BRANCH = 'main';
|
||||||
|
process.env.BYTELYST_BUILT_AT = '2026-01-01T00:00:00Z';
|
||||||
|
const info = getBuildInfo();
|
||||||
|
expect(info.commitSha).toBe('abcdef0');
|
||||||
|
expect(info.commitShaFull).toBe('abcdef0123456789');
|
||||||
|
expect(info.branch).toBe('main');
|
||||||
|
expect(info.builtAt).toBe('2026-01-01T00:00:00Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives short SHA from full SHA when only full is set', () => {
|
||||||
|
delete process.env.BYTELYST_COMMIT_SHA;
|
||||||
|
process.env.BYTELYST_COMMIT_SHA_FULL = '1234567890abcdef';
|
||||||
|
const info = getBuildInfo();
|
||||||
|
expect(info.commitSha).toBe('1234567');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null fields when env vars are missing', () => {
|
||||||
|
delete process.env.BYTELYST_COMMIT_SHA;
|
||||||
|
delete process.env.BYTELYST_COMMIT_SHA_FULL;
|
||||||
|
delete process.env.BYTELYST_BRANCH;
|
||||||
|
delete process.env.BYTELYST_BUILT_AT;
|
||||||
|
const info = getBuildInfo();
|
||||||
|
expect(info.commitSha).toBeNull();
|
||||||
|
expect(info.branch).toBeNull();
|
||||||
|
expect(info.builtAt).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRuntimeInfo', () => {
|
||||||
|
it('returns process metadata', () => {
|
||||||
|
const info = getRuntimeInfo();
|
||||||
|
expect(info.nodeVersion).toMatch(/^v\d/);
|
||||||
|
expect(info.platform).toBeTruthy();
|
||||||
|
expect(info.pid).toBeGreaterThan(0);
|
||||||
|
expect(info.memoryMb).toBeGreaterThan(0);
|
||||||
|
expect(info.uptimeSeconds).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(info.uptimeHuman).toMatch(/^\d/);
|
||||||
|
expect(new Date(info.startedAt).toString()).not.toBe('Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('humanizes uptime correctly', () => {
|
||||||
|
const info = getRuntimeInfo();
|
||||||
|
// Some format containing s/m/h/d
|
||||||
|
expect(info.uptimeHuman).toMatch(/^\d+(s|m|h|d)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigInfo', () => {
|
||||||
|
it('lists safe env keys, excluding secrets', () => {
|
||||||
|
process.env.PRODUCT_ID = 'test';
|
||||||
|
process.env.PLATFORM_API_URL = 'http://x';
|
||||||
|
process.env.PLATFORM_JWT_SECRET = 'shh'; // should be excluded by SECRET pattern
|
||||||
|
process.env.SOMETHING_ELSE = 'not-listed'; // not in prefix list
|
||||||
|
const info = getConfigInfo({
|
||||||
|
productId: 'test',
|
||||||
|
serviceName: 'svc',
|
||||||
|
serviceVersion: '1.0.0',
|
||||||
|
});
|
||||||
|
expect(info.envKeys).toContain('PRODUCT_ID');
|
||||||
|
expect(info.envKeys).toContain('PLATFORM_API_URL');
|
||||||
|
expect(info.envKeys).not.toContain('PLATFORM_JWT_SECRET');
|
||||||
|
expect(info.envKeys).not.toContain('SOMETHING_ELSE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses NODE_ENV or defaults to development', () => {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
let info = getConfigInfo({ productId: 'x', serviceName: 'y', serviceVersion: '0.0.0' });
|
||||||
|
expect(info.nodeEnv).toBe('production');
|
||||||
|
|
||||||
|
delete process.env.NODE_ENV;
|
||||||
|
info = getConfigInfo({ productId: 'x', serviceName: 'y', serviceVersion: '0.0.0' });
|
||||||
|
expect(info.nodeEnv).toBe('development');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects custom safeEnvPrefixes', () => {
|
||||||
|
process.env.MY_CUSTOM_VAR = 'x';
|
||||||
|
const info = getConfigInfo({
|
||||||
|
productId: 'x',
|
||||||
|
serviceName: 'y',
|
||||||
|
serviceVersion: '0.0.0',
|
||||||
|
safeEnvPrefixes: ['MY_CUSTOM_'],
|
||||||
|
});
|
||||||
|
expect(info.envKeys).toEqual(['MY_CUSTOM_VAR']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readServiceVersion', () => {
|
||||||
|
it('returns a non-empty version string', () => {
|
||||||
|
// Walks up from current file location and finds a package.json.
|
||||||
|
const v = readServiceVersion(import.meta.url);
|
||||||
|
expect(v).toMatch(/^\d/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to "0.0.0" when nothing is found', () => {
|
||||||
|
delete process.env.npm_package_version;
|
||||||
|
// Walk up from /tmp (no package.json above it inside the test fs)
|
||||||
|
const v = readServiceVersion('/no-such-dir/file.js');
|
||||||
|
expect(v).toBe('0.0.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dependencyCheck', () => {
|
||||||
|
it('marks ok=true with latency when check resolves', async () => {
|
||||||
|
const status = await dependencyCheck('test', async () => {
|
||||||
|
await new Promise(r => setTimeout(r, 5));
|
||||||
|
});
|
||||||
|
expect(status.ok).toBe(true);
|
||||||
|
expect(status.name).toBe('test');
|
||||||
|
expect(status.latencyMs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(status.detail).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks ok=false with detail when check rejects', async () => {
|
||||||
|
const status = await dependencyCheck('test', async () => {
|
||||||
|
throw new Error('boom');
|
||||||
|
});
|
||||||
|
expect(status.ok).toBe(false);
|
||||||
|
expect(status.detail).toBe('boom');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('times out long-running checks', async () => {
|
||||||
|
const status = await dependencyCheck('slow', () => new Promise(r => setTimeout(r, 1000)), 50);
|
||||||
|
expect(status.ok).toBe(false);
|
||||||
|
expect(status.detail).toMatch(/timed out/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collectDevopsInfo', () => {
|
||||||
|
it('returns full payload with all sections', async () => {
|
||||||
|
const info = await collectDevopsInfo({
|
||||||
|
productId: 'p',
|
||||||
|
serviceName: 's',
|
||||||
|
serviceVersion: '0.0.0',
|
||||||
|
});
|
||||||
|
expect(info.build).toBeDefined();
|
||||||
|
expect(info.runtime).toBeDefined();
|
||||||
|
expect(info.config).toBeDefined();
|
||||||
|
expect(info.config.productId).toBe('p');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('populates dependencies when checks are provided', async () => {
|
||||||
|
const info = await collectDevopsInfo({
|
||||||
|
productId: 'p',
|
||||||
|
serviceName: 's',
|
||||||
|
serviceVersion: '0.0.0',
|
||||||
|
dependencyChecks: [
|
||||||
|
() => dependencyCheck('a', async () => undefined),
|
||||||
|
() =>
|
||||||
|
dependencyCheck('b', async () => {
|
||||||
|
throw new Error('fail');
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(info.dependencies).toHaveLength(2);
|
||||||
|
expect(info.dependencies?.[0]?.name).toBe('a');
|
||||||
|
expect(info.dependencies?.[0]?.ok).toBe(true);
|
||||||
|
expect(info.dependencies?.[1]?.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through extra fields', async () => {
|
||||||
|
const info = await collectDevopsInfo({
|
||||||
|
productId: 'p',
|
||||||
|
serviceName: 's',
|
||||||
|
serviceVersion: '0.0.0',
|
||||||
|
extra: { customField: 'value', count: 42 },
|
||||||
|
});
|
||||||
|
expect(info.extra).toEqual({ customField: 'value', count: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not leak secret env values in config.envKeys', async () => {
|
||||||
|
process.env.PLATFORM_FAKE_SECRET = 'super-secret';
|
||||||
|
const info = await collectDevopsInfo({
|
||||||
|
productId: 'p',
|
||||||
|
serviceName: 's',
|
||||||
|
serviceVersion: '0.0.0',
|
||||||
|
});
|
||||||
|
// Key is in PLATFORM_ prefix BUT matches SECRET pattern → excluded.
|
||||||
|
expect(info.config.envKeys).not.toContain('PLATFORM_FAKE_SECRET');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,48 +3,40 @@
|
|||||||
*
|
*
|
||||||
* Usage in a Node backend:
|
* Usage in a Node backend:
|
||||||
* ```ts
|
* ```ts
|
||||||
* import { collectDevopsInfo } from '@bytelyst/devops/server';
|
* import { collectDevopsInfo, readServiceVersion, dependencyCheck } from '@bytelyst/devops/server';
|
||||||
*
|
*
|
||||||
* app.get('/api/devops/info', async (req, res) => {
|
* app.get('/api/devops/info', requireAdmin, async (_req, res) => {
|
||||||
* const info = await collectDevopsInfo({
|
* const info = await collectDevopsInfo({
|
||||||
* productId: 'invttrdg',
|
* productId: 'invttrdg',
|
||||||
* serviceName: 'trading-backend',
|
* serviceName: 'trading-backend',
|
||||||
* serviceVersion: pkg.version,
|
* serviceVersion: readServiceVersion(import.meta.url),
|
||||||
|
* dependencyChecks: [
|
||||||
|
* () => dependencyCheck('cosmos', () => cosmosPing()),
|
||||||
|
* ],
|
||||||
* });
|
* });
|
||||||
* res.json(info);
|
* res.json(info);
|
||||||
* });
|
* });
|
||||||
|
*
|
||||||
|
* app.get('/api/devops/version', (_req, res) => res.json(getBuildInfo()));
|
||||||
* ```
|
* ```
|
||||||
*
|
|
||||||
* Build-time values (commit SHA, branch, build time, image) are read from env vars:
|
|
||||||
* - BYTELYST_COMMIT_SHA / BYTELYST_COMMIT_SHA_FULL
|
|
||||||
* - BYTELYST_BRANCH
|
|
||||||
* - BYTELYST_BUILT_AT (ISO 8601)
|
|
||||||
* - BYTELYST_COMMIT_AUTHOR
|
|
||||||
* - BYTELYST_COMMIT_MESSAGE
|
|
||||||
* - BYTELYST_DOCKER_IMAGE
|
|
||||||
*
|
|
||||||
* Dockerfiles should bake these via ARG/ENV or a build script.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo } from './types.js';
|
import type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo } from './types.js';
|
||||||
|
|
||||||
export type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo };
|
export type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo };
|
||||||
|
|
||||||
/** Options for {@link collectDevopsInfo}. */
|
/** Options for {@link collectDevopsInfo}. */
|
||||||
export interface CollectOptions {
|
export interface CollectOptions {
|
||||||
/** Required: product identifier (e.g. "invttrdg"). */
|
|
||||||
productId: string;
|
productId: string;
|
||||||
/** Required: service name (e.g. "trading-backend"). */
|
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
/** Required: service version from package.json. */
|
|
||||||
serviceVersion: string;
|
serviceVersion: string;
|
||||||
/** Optional: prefixes of env var keys that are safe to list (defaults to NODE_ENV, PRODUCT_ID, PLATFORM_, VITE_, BYTELYST_). */
|
|
||||||
safeEnvPrefixes?: string[];
|
safeEnvPrefixes?: string[];
|
||||||
/** Optional: async health checks for external dependencies. */
|
|
||||||
dependencyChecks?: Array<() => Promise<DependencyStatus>>;
|
dependencyChecks?: Array<() => Promise<DependencyStatus>>;
|
||||||
/** Optional: extra service-specific fields. */
|
|
||||||
extra?: Record<string, unknown>;
|
extra?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,22 +51,27 @@ const DEFAULT_SAFE_PREFIXES = [
|
|||||||
'PORT',
|
'PORT',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const SECRET_PATTERN = /SECRET|KEY|TOKEN|PASSWORD|PRIVATE/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect runtime + build info. Safe to call from any request handler.
|
* Collect runtime + build info. Safe to call from any request handler.
|
||||||
*
|
*
|
||||||
* Never returns secret values; only env var *keys* are listed.
|
* Never returns secret values; only env var *keys* are listed.
|
||||||
*/
|
*/
|
||||||
export async function collectDevopsInfo(opts: CollectOptions): Promise<DevopsInfo> {
|
export async function collectDevopsInfo(opts: CollectOptions): Promise<DevopsInfo> {
|
||||||
const build = collectBuildInfo();
|
const build = getBuildInfo();
|
||||||
const runtime = collectRuntimeInfo();
|
const runtime = getRuntimeInfo();
|
||||||
const config = collectConfigInfo(opts);
|
const config = getConfigInfo(opts);
|
||||||
const dependencies = opts.dependencyChecks
|
const dependencies = opts.dependencyChecks
|
||||||
? await runDependencyChecks(opts.dependencyChecks)
|
? await runDependencyChecks(opts.dependencyChecks)
|
||||||
: undefined;
|
: undefined;
|
||||||
return { build, runtime, config, dependencies, extra: opts.extra };
|
return { build, runtime, config, dependencies, extra: opts.extra };
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectBuildInfo(): BuildInfo {
|
/**
|
||||||
|
* Build-only info — safe to expose without auth (commit SHA + version).
|
||||||
|
*/
|
||||||
|
export function getBuildInfo(): BuildInfo {
|
||||||
const full = process.env.BYTELYST_COMMIT_SHA_FULL || process.env.BYTELYST_COMMIT_SHA || null;
|
const full = process.env.BYTELYST_COMMIT_SHA_FULL || process.env.BYTELYST_COMMIT_SHA || null;
|
||||||
const short = process.env.BYTELYST_COMMIT_SHA || (full ? full.substring(0, 7) : null);
|
const short = process.env.BYTELYST_COMMIT_SHA || (full ? full.substring(0, 7) : null);
|
||||||
return {
|
return {
|
||||||
@ -88,7 +85,8 @@ function collectBuildInfo(): BuildInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectRuntimeInfo(): RuntimeInfo {
|
/** Runtime info (process + container). */
|
||||||
|
export function getRuntimeInfo(): RuntimeInfo {
|
||||||
const uptimeSeconds = Math.floor(process.uptime());
|
const uptimeSeconds = Math.floor(process.uptime());
|
||||||
const mem = process.memoryUsage();
|
const mem = process.memoryUsage();
|
||||||
return {
|
return {
|
||||||
@ -105,11 +103,12 @@ function collectRuntimeInfo(): RuntimeInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectConfigInfo(opts: CollectOptions): ConfigInfo {
|
/** Configuration-level info (safe, non-secret env keys only). */
|
||||||
|
export function getConfigInfo(opts: CollectOptions): ConfigInfo {
|
||||||
const prefixes = opts.safeEnvPrefixes ?? DEFAULT_SAFE_PREFIXES;
|
const prefixes = opts.safeEnvPrefixes ?? DEFAULT_SAFE_PREFIXES;
|
||||||
const envKeys = Object.keys(process.env)
|
const envKeys = Object.keys(process.env)
|
||||||
.filter(k => prefixes.some(p => (p.endsWith('_') ? k.startsWith(p) : k === p)))
|
.filter(k => prefixes.some(p => (p.endsWith('_') ? k.startsWith(p) : k === p)))
|
||||||
.filter(k => !/SECRET|KEY|TOKEN|PASSWORD|PRIVATE/i.test(k))
|
.filter(k => !SECRET_PATTERN.test(k))
|
||||||
.sort();
|
.sort();
|
||||||
return {
|
return {
|
||||||
productId: opts.productId,
|
productId: opts.productId,
|
||||||
@ -120,6 +119,81 @@ function collectConfigInfo(opts: CollectOptions): ConfigInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk up from the given path/import.meta.url to find package.json and return its `version`.
|
||||||
|
* Falls back to `npm_package_version` env var, then to "0.0.0".
|
||||||
|
*/
|
||||||
|
export function readServiceVersion(fromPathOrUrl?: string): string {
|
||||||
|
let dir: string;
|
||||||
|
if (!fromPathOrUrl) {
|
||||||
|
dir = process.cwd();
|
||||||
|
} else if (fromPathOrUrl.startsWith('file://')) {
|
||||||
|
dir = dirname(fileURLToPath(fromPathOrUrl));
|
||||||
|
} else {
|
||||||
|
dir = dirname(fromPathOrUrl);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const pkg = join(dir, 'package.json');
|
||||||
|
if (existsSync(pkg)) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(readFileSync(pkg, 'utf8')) as { version?: string };
|
||||||
|
if (data.version) return data.version;
|
||||||
|
} catch {
|
||||||
|
/* fall through */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
return process.env.npm_package_version || '0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an async health check with timing + timeout + error-to-status conversion.
|
||||||
|
*/
|
||||||
|
export async function dependencyCheck(
|
||||||
|
name: string,
|
||||||
|
check: () => Promise<unknown>,
|
||||||
|
timeoutMs = 5000
|
||||||
|
): Promise<DependencyStatus> {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
check(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return { name, ok: true, latencyMs: Date.now() - start };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
ok: false,
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience HTTP ping — returns ok if status < 500. */
|
||||||
|
export async function httpDependencyCheck(
|
||||||
|
name: string,
|
||||||
|
url: string,
|
||||||
|
timeoutMs = 5000
|
||||||
|
): Promise<DependencyStatus> {
|
||||||
|
return dependencyCheck(
|
||||||
|
name,
|
||||||
|
async () => {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (res.status >= 500) {
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeoutMs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runDependencyChecks(
|
async function runDependencyChecks(
|
||||||
checks: Array<() => Promise<DependencyStatus>>
|
checks: Array<() => Promise<DependencyStatus>>
|
||||||
): Promise<DependencyStatus[]> {
|
): Promise<DependencyStatus[]> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user