Add Railway deployment configuration and update health endpoint
Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
coverage
|
coverage
|
||||||
|
dist
|
||||||
|
|||||||
85
README.md
85
README.md
@@ -1 +1,84 @@
|
|||||||
# blackroad-os
|
# BlackRoad OS
|
||||||
|
|
||||||
|
A microservice infrastructure management platform for the BlackRoad ecosystem.
|
||||||
|
|
||||||
|
## Service Purpose
|
||||||
|
|
||||||
|
BlackRoad OS provides:
|
||||||
|
- Health monitoring endpoints
|
||||||
|
- Version tracking and build information
|
||||||
|
- Express and Fastify-based API routing
|
||||||
|
- Job scheduling with BullMQ
|
||||||
|
- React component library for UI dashboards
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 18+
|
||||||
|
- npm
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start at `http://localhost:8080`.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build & Deploy
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles TypeScript to JavaScript in the `dist/` directory.
|
||||||
|
|
||||||
|
### Running in Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Railway Deployment
|
||||||
|
|
||||||
|
This project is configured for automatic deployment on Railway. The `railway.toml` file defines:
|
||||||
|
|
||||||
|
- **Builder**: Nixpacks
|
||||||
|
- **Start Command**: `npm run start`
|
||||||
|
- **Healthcheck**: `/health` endpoint
|
||||||
|
- **Default Port**: 8080
|
||||||
|
|
||||||
|
## Healthcheck
|
||||||
|
|
||||||
|
The `/health` endpoint returns:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "blackroad-os"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `PORT` | Server port | `8080` |
|
||||||
|
| `SERVICE_NAME` | Service identifier | `blackroad-os` |
|
||||||
|
| `ENVIRONMENT` | Runtime environment | `production` |
|
||||||
|
| `APP_VERSION` | Application version | `1.0.0` |
|
||||||
|
| `APP_COMMIT` | Git commit hash | Auto-detected |
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export async function GET() {
|
|
||||||
return Response.json({ status: "ok" });
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { getBuildInfo } from "../../../src/utils/buildInfo";
|
|
||||||
export async function GET() {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
return Response.json({ version: info.version, commit: info.commit });
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
||||||
import { StatusPill } from "./StatusPill";
|
|
||||||
export function EnvCard({ env }) {
|
|
||||||
return (_jsxs("div", { className: "env-card", children: [_jsx("div", { className: "env-region", style: { textTransform: "uppercase", fontSize: "0.85rem" }, children: env.region }), _jsx("h2", { children: env.name }), _jsxs("div", { children: ["Env ID: ", env.id] }), _jsx(StatusPill, { status: env.status })] }));
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
const statusConfig = {
|
|
||||||
healthy: { label: "Healthy", className: "status-pill status-pill--healthy" },
|
|
||||||
degraded: { label: "Degraded", className: "status-pill status-pill--degraded" },
|
|
||||||
down: { label: "Down", className: "status-pill status-pill--down" }
|
|
||||||
};
|
|
||||||
export function StatusPill({ status }) {
|
|
||||||
const config = statusConfig[status];
|
|
||||||
return _jsx("span", { className: config.className, children: config.label });
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const mockEnvironments = [
|
|
||||||
{ id: "env_1", name: "Development", region: "us-east-1", status: "healthy" },
|
|
||||||
{ id: "env_2", name: "Staging", region: "eu-west-1", status: "degraded" }
|
|
||||||
];
|
|
||||||
export async function getEnvironments() {
|
|
||||||
return mockEnvironments;
|
|
||||||
}
|
|
||||||
export async function getEnvById(id) {
|
|
||||||
return mockEnvironments.find((env) => env.id === id);
|
|
||||||
}
|
|
||||||
export async function getHealth() {
|
|
||||||
return { status: "ok", uptime: process.uptime() };
|
|
||||||
}
|
|
||||||
export async function getVersion() {
|
|
||||||
const version = process.env.APP_VERSION || "1.0.0";
|
|
||||||
const commit = process.env.APP_COMMIT || "unknown";
|
|
||||||
return { version, commit };
|
|
||||||
}
|
|
||||||
108
package-lock.json
generated
108
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/express": "^5.0.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.2.6",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -1779,6 +1780,17 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/body-parser": {
|
||||||
|
"version": "1.19.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/connect": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -1790,6 +1802,16 @@
|
|||||||
"assertion-error": "^2.0.1"
|
"assertion-error": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/connect": {
|
||||||
|
"version": "3.4.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/cookiejar": {
|
"node_modules/@types/cookiejar": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||||
@@ -1811,6 +1833,38 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/express": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/body-parser": "*",
|
||||||
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
|
"@types/serve-static": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/express-serve-static-core": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/qs": "*",
|
||||||
|
"@types/range-parser": "*",
|
||||||
|
"@types/send": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/http-errors": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/methods": {
|
"node_modules/@types/methods": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||||
@@ -1818,6 +1872,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mime": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
@@ -1828,6 +1889,20 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/range-parser": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.6",
|
"version": "19.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||||
@@ -1848,6 +1923,39 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/serve-static": {
|
||||||
|
"version": "1.15.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||||
|
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-errors": "*",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/send": "<1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/serve-static/node_modules/@types/send": {
|
||||||
|
"version": "0.17.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||||
|
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/mime": "^1",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/superagent": {
|
"node_modules/@types/superagent": {
|
||||||
"version": "8.1.9",
|
"version": "8.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "blackroad-os",
|
"name": "blackroad-os",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "BlackRoad OS - A microservice infrastructure management platform",
|
||||||
"main": "index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "tsc"
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node src/index.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/express": "^5.0.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.2.6",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
12
railway.toml
Normal file
12
railway.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[build]
|
||||||
|
builder = "NIXPACKS"
|
||||||
|
|
||||||
|
[deploy]
|
||||||
|
startCommand = "npm run start"
|
||||||
|
healthcheckPath = "/health"
|
||||||
|
restartPolicy = "on-failure"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
SERVICE_NAME = "blackroad-os"
|
||||||
|
PORT = "8080"
|
||||||
|
ENVIRONMENT = "production"
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import { createMetaRouter } from "./routes/meta";
|
|
||||||
export function createApp() {
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use("/internal", createMetaRouter());
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import cron from "node-cron";
|
|
||||||
import { Queue } from "bullmq";
|
|
||||||
export function buildHeartbeatQueue(connection = { host: "localhost", port: 6379 }) {
|
|
||||||
return new Queue("heartbeat", { connection });
|
|
||||||
}
|
|
||||||
let defaultQueue = null;
|
|
||||||
function getDefaultQueue() {
|
|
||||||
if (!defaultQueue) {
|
|
||||||
defaultQueue = buildHeartbeatQueue();
|
|
||||||
}
|
|
||||||
return defaultQueue;
|
|
||||||
}
|
|
||||||
export async function enqueueHeartbeat(queue = getDefaultQueue()) {
|
|
||||||
const payload = { ts: Date.now() };
|
|
||||||
await queue.add("heartbeat", payload);
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
export function startHeartbeatScheduler(queue = getDefaultQueue()) {
|
|
||||||
const task = cron.schedule("*/5 * * * *", () => {
|
|
||||||
enqueueHeartbeat(queue);
|
|
||||||
});
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
23
src/index.js
23
src/index.js
@@ -1,23 +0,0 @@
|
|||||||
import Fastify from "fastify";
|
|
||||||
import { getBuildInfo } from "./utils/buildInfo";
|
|
||||||
export async function createServer() {
|
|
||||||
const server = Fastify({ logger: true });
|
|
||||||
server.get("/health", async () => ({ status: "ok" }));
|
|
||||||
server.get("/version", async () => {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
return { version: info.version, commit: info.commit };
|
|
||||||
});
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
if (require.main === module) {
|
|
||||||
const port = Number(process.env.PORT || 3000);
|
|
||||||
createServer()
|
|
||||||
.then((server) => server.listen({ port, host: "0.0.0.0" }))
|
|
||||||
.then((address) => {
|
|
||||||
console.log(`Server listening at ${address}`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import { getBuildInfo } from "./utils/buildInfo";
|
import { getBuildInfo } from "./utils/buildInfo";
|
||||||
|
|
||||||
|
const SERVICE_NAME = process.env.SERVICE_NAME || "blackroad-os";
|
||||||
|
|
||||||
export async function createServer() {
|
export async function createServer() {
|
||||||
const server = Fastify({ logger: true });
|
const server = Fastify({ logger: true });
|
||||||
|
|
||||||
server.get("/health", async () => ({ status: "ok" }));
|
server.get("/health", async () => ({
|
||||||
|
status: "ok",
|
||||||
|
service: SERVICE_NAME
|
||||||
|
}));
|
||||||
|
|
||||||
server.get("/version", async () => {
|
server.get("/version", async () => {
|
||||||
const info = getBuildInfo();
|
const info = getBuildInfo();
|
||||||
@@ -15,7 +20,7 @@ export async function createServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const port = Number(process.env.PORT || 3000);
|
const port = Number(process.env.PORT || 8080);
|
||||||
createServer()
|
createServer()
|
||||||
.then((server) => server.listen({ port, host: "0.0.0.0" }))
|
.then((server) => server.listen({ port, host: "0.0.0.0" }))
|
||||||
.then((address) => {
|
.then((address) => {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Worker } from "bullmq";
|
|
||||||
export function registerSampleJobProcessor(connection = { host: "localhost", port: 6379 }) {
|
|
||||||
const worker = new Worker("sample", async (job) => {
|
|
||||||
console.log(`Processing job ${job.id}`);
|
|
||||||
return job.data;
|
|
||||||
}, { connection });
|
|
||||||
worker.on("failed", (job, err) => {
|
|
||||||
console.error(`Job ${job?.id} failed`, err);
|
|
||||||
});
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import { getBuildInfo } from "../utils/buildInfo";
|
|
||||||
export function createMetaRouter() {
|
|
||||||
const router = Router();
|
|
||||||
router.get("/health", (_req, res) => {
|
|
||||||
res.json({ status: "ok" });
|
|
||||||
});
|
|
||||||
router.get("/version", (_req, res) => {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
res.json({ version: info.version, commit: info.commit });
|
|
||||||
});
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import * as childProcess from "child_process";
|
|
||||||
export function readGitCommit() {
|
|
||||||
try {
|
|
||||||
return childProcess.execSync("git rev-parse HEAD", { stdio: "pipe" }).toString().trim();
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function getBuildInfo(gitReader = readGitCommit) {
|
|
||||||
const version = process.env.APP_VERSION || "1.0.0";
|
|
||||||
const commit = process.env.APP_COMMIT || gitReader() || "unknown";
|
|
||||||
const buildTime = new Date().toISOString();
|
|
||||||
return { version, commit, buildTime };
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import request from "supertest";
|
|
||||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
||||||
import { createApp } from "../src/app";
|
|
||||||
import { createServer } from "../src/index";
|
|
||||||
vi.mock("../src/utils/buildInfo", () => ({
|
|
||||||
getBuildInfo: () => ({ version: "test-version", commit: "test-commit", buildTime: "now" })
|
|
||||||
}));
|
|
||||||
describe("Express internal routes", () => {
|
|
||||||
const app = createApp();
|
|
||||||
it("returns health", async () => {
|
|
||||||
const response = await request(app).get("/internal/health");
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version", async () => {
|
|
||||||
const response = await request(app).get("/internal/version");
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ version: "test-version", commit: "test-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("Fastify public routes", () => {
|
|
||||||
let server;
|
|
||||||
beforeEach(async () => {
|
|
||||||
server = await createServer();
|
|
||||||
});
|
|
||||||
it("returns health", async () => {
|
|
||||||
const response = await server.inject({ method: "GET", url: "/health" });
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
expect(response.json()).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version", async () => {
|
|
||||||
const response = await server.inject({ method: "GET", url: "/version" });
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
expect(response.json()).toEqual({ version: "test-version", commit: "test-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -33,7 +33,7 @@ describe("Fastify public routes", () => {
|
|||||||
it("returns health", async () => {
|
it("returns health", async () => {
|
||||||
const response = await server.inject({ method: "GET", url: "/health" });
|
const response = await server.inject({ method: "GET", url: "/health" });
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
expect(response.json()).toEqual({ status: "ok" });
|
expect(response.json()).toEqual({ status: "ok", service: "blackroad-os" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns version", async () => {
|
it("returns version", async () => {
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
||||||
import { getBuildInfo } from "../src/utils/buildInfo";
|
|
||||||
const originalEnv = { ...process.env };
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
describe("getBuildInfo", () => {
|
|
||||||
it("uses env vars when provided", () => {
|
|
||||||
process.env.APP_VERSION = "3.0.0";
|
|
||||||
process.env.APP_COMMIT = "xyz";
|
|
||||||
const info = getBuildInfo();
|
|
||||||
expect(info.version).toBe("3.0.0");
|
|
||||||
expect(info.commit).toBe("xyz");
|
|
||||||
expect(new Date(info.buildTime).toString()).not.toBe("Invalid Date");
|
|
||||||
});
|
|
||||||
it("falls back to git when env missing", () => {
|
|
||||||
const gitReader = vi.fn().mockReturnValue("abcdef");
|
|
||||||
delete process.env.APP_COMMIT;
|
|
||||||
const info = getBuildInfo(gitReader);
|
|
||||||
expect(info.commit).toBe("abcdef");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { EnvCard } from "../components/EnvCard";
|
|
||||||
describe("EnvCard", () => {
|
|
||||||
const env = {
|
|
||||||
id: "env_123",
|
|
||||||
name: "Production",
|
|
||||||
region: "us-west-2",
|
|
||||||
status: "healthy"
|
|
||||||
};
|
|
||||||
it("renders name, region, and id", () => {
|
|
||||||
render(_jsx(EnvCard, { env: env }));
|
|
||||||
expect(screen.getByText(env.region)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(env.name)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(`Env ID: ${env.id}`)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { getEnvironments, getEnvById, getHealth, getVersion } from "../lib/fetcher";
|
|
||||||
const originalEnv = { ...process.env };
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
});
|
|
||||||
describe("fetcher", () => {
|
|
||||||
it("returns mock environments", async () => {
|
|
||||||
const envs = await getEnvironments();
|
|
||||||
expect(envs).toHaveLength(2);
|
|
||||||
expect(envs[0]).toEqual(expect.objectContaining({ id: "env_1", name: "Development", region: "us-east-1" }));
|
|
||||||
});
|
|
||||||
it("returns environment by id", async () => {
|
|
||||||
const env = await getEnvById("env_2");
|
|
||||||
expect(env?.name).toBe("Staging");
|
|
||||||
expect(await getEnvById("missing"))?.toBeUndefined();
|
|
||||||
});
|
|
||||||
it("returns health with uptime", async () => {
|
|
||||||
vi.spyOn(process, "uptime").mockReturnValue(42);
|
|
||||||
const health = await getHealth();
|
|
||||||
expect(health).toEqual({ status: "ok", uptime: 42 });
|
|
||||||
});
|
|
||||||
it("returns version info", async () => {
|
|
||||||
process.env.APP_VERSION = "2.0.0";
|
|
||||||
process.env.APP_COMMIT = "abc123";
|
|
||||||
const info = await getVersion();
|
|
||||||
expect(info).toEqual({ version: "2.0.0", commit: "abc123" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
||||||
import cron from "node-cron";
|
|
||||||
import { startHeartbeatScheduler } from "../src/heartbeat";
|
|
||||||
vi.mock("node-cron", () => {
|
|
||||||
return {
|
|
||||||
default: {
|
|
||||||
schedule: vi.fn((expression, callback) => ({ fireOnTick: callback, expression }))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe("startHeartbeatScheduler", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
it("schedules heartbeat every five minutes and enqueues payload", async () => {
|
|
||||||
const add = vi.fn();
|
|
||||||
const task = startHeartbeatScheduler({ add });
|
|
||||||
expect(cron.schedule).toHaveBeenCalledWith("*/5 * * * *", expect.any(Function));
|
|
||||||
// fire the cron callback
|
|
||||||
task.fireOnTick();
|
|
||||||
expect(add).toHaveBeenCalledWith("heartbeat", expect.objectContaining({ ts: expect.any(Number) }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { GET as health } from "../app/api/health/route";
|
|
||||||
import { GET as version } from "../app/api/version/route";
|
|
||||||
vi.mock("../src/utils/buildInfo", () => ({
|
|
||||||
getBuildInfo: () => ({ version: "api-version", commit: "api-commit", buildTime: "now" })
|
|
||||||
}));
|
|
||||||
describe("Next API routes", () => {
|
|
||||||
it("returns health response", async () => {
|
|
||||||
const res = await health();
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json()).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version response", async () => {
|
|
||||||
const res = await version();
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json()).toEqual({ version: "api-version", commit: "api-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { registerSampleJobProcessor } from "../src/jobs/sample.job";
|
|
||||||
vi.mock("bullmq", () => {
|
|
||||||
class MockWorker {
|
|
||||||
constructor(_name, processor, _opts) {
|
|
||||||
this.handlers = {};
|
|
||||||
this.processor = processor;
|
|
||||||
}
|
|
||||||
on(event, handler) {
|
|
||||||
this.handlers[event] = this.handlers[event] || [];
|
|
||||||
this.handlers[event].push(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { Worker: MockWorker };
|
|
||||||
});
|
|
||||||
describe("registerSampleJobProcessor", () => {
|
|
||||||
it("registers worker and handlers", () => {
|
|
||||||
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
||||||
const worker = registerSampleJobProcessor({ host: "localhost", port: 6379 });
|
|
||||||
expect(worker.processor).toBeInstanceOf(Function);
|
|
||||||
expect(worker.handlers.failed).toHaveLength(1);
|
|
||||||
// simulate processing and failure
|
|
||||||
worker.processor({ id: 1, data: { hello: "world" } });
|
|
||||||
worker.handlers.failed[0]({ id: 1 }, new Error("boom"));
|
|
||||||
expect(consoleLog).toHaveBeenCalledWith("Processing job 1");
|
|
||||||
expect(consoleError).toHaveBeenCalled();
|
|
||||||
consoleLog.mockRestore();
|
|
||||||
consoleError.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { StatusPill } from "../components/StatusPill";
|
|
||||||
describe("StatusPill", () => {
|
|
||||||
const cases = [
|
|
||||||
["healthy", "Healthy", "status-pill--healthy"],
|
|
||||||
["degraded", "Degraded", "status-pill--degraded"],
|
|
||||||
["down", "Down", "status-pill--down"]
|
|
||||||
];
|
|
||||||
it.each(cases)("renders %s status", (status, label, className) => {
|
|
||||||
render(_jsx(StatusPill, { status: status }));
|
|
||||||
const pill = screen.getByText(label);
|
|
||||||
expect(pill).toBeInTheDocument();
|
|
||||||
expect(pill.className).toContain(className);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "ESNext",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"skipLibCheck": true,
|
||||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
||||||
},
|
},
|
||||||
"include": ["src", "lib", "components", "tests", "app", "vitest.config.ts"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
test: {
|
|
||||||
environment: "jsdom",
|
|
||||||
setupFiles: "./vitest.setup.ts",
|
|
||||||
globals: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user