mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-18 01:34:00 -05:00
Add deployment orchestration skeleton
This commit is contained in:
39
.github/workflows/deploy-all.yml
vendored
Normal file
39
.github/workflows/deploy-all.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Deploy BlackRoad OS
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
serviceId:
|
||||||
|
description: "Optional service id to deploy (core, api, operator, agents, console, web, docs)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.serviceId }}" ]; then
|
||||||
|
echo "Deploying single service: ${{ github.event.inputs.serviceId }}"
|
||||||
|
npm run deploy:service -- ${{ github.event.inputs.serviceId }}
|
||||||
|
else
|
||||||
|
echo "Deploying all services"
|
||||||
|
npm run deploy:all
|
||||||
|
fi
|
||||||
36
docs/DEPLOYMENT.md
Normal file
36
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# BlackRoad OS Deployment
|
||||||
|
|
||||||
|
This repo is the *orchestrator* for all BlackRoad OS services.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `npm run deploy:all`
|
||||||
|
Deploys all services (core, api, operator, agents, console, web, docs) via Railway.
|
||||||
|
|
||||||
|
- `npm run deploy:service -- core`
|
||||||
|
Deploy only a single service by `id`.
|
||||||
|
|
||||||
|
- `npm run health:all`
|
||||||
|
Check health endpoints for all services using their public domains.
|
||||||
|
|
||||||
|
## Service Registry
|
||||||
|
|
||||||
|
All services are defined in `infra/services.json`.
|
||||||
|
|
||||||
|
To add or change a service:
|
||||||
|
|
||||||
|
1. Edit `infra/services.json` and update:
|
||||||
|
- `id`
|
||||||
|
- `railwayProject`
|
||||||
|
- `railwayService`
|
||||||
|
- `domain`
|
||||||
|
- `healthPath`
|
||||||
|
2. Make sure the Railway project/service names match.
|
||||||
|
3. Commit and push your changes.
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
The workflow `.github/workflows/deploy-all.yml` lets you:
|
||||||
|
|
||||||
|
- Trigger **Deploy BlackRoad OS** from the Actions tab.
|
||||||
|
- Optionally pass `serviceId` to deploy just one service.
|
||||||
62
infra/services.json
Normal file
62
infra/services.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "core",
|
||||||
|
"name": "BlackRoad OS Core",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-core",
|
||||||
|
"kind": "backend",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "core",
|
||||||
|
"domain": "core.blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api",
|
||||||
|
"name": "BlackRoad OS API Gateway",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-api",
|
||||||
|
"kind": "backend",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "api",
|
||||||
|
"domain": "api.blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "operator",
|
||||||
|
"name": "BlackRoad OS Operator",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-operator",
|
||||||
|
"kind": "worker",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "operator",
|
||||||
|
"domain": "operator.blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "console",
|
||||||
|
"name": "BlackRoad OS Prism Console",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-prism-console",
|
||||||
|
"kind": "frontend",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "console",
|
||||||
|
"domain": "console.blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "web",
|
||||||
|
"name": "BlackRoad OS Web",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-web",
|
||||||
|
"kind": "frontend",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "app",
|
||||||
|
"domain": "blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "docs",
|
||||||
|
"name": "BlackRoad OS Docs",
|
||||||
|
"repo": "https://github.com/BlackRoad-OS/blackroad-os-docs",
|
||||||
|
"kind": "frontend",
|
||||||
|
"railwayProject": "blackroad-operating-system",
|
||||||
|
"railwayService": "docs",
|
||||||
|
"domain": "docs.blackroad.systems",
|
||||||
|
"healthPath": "/health"
|
||||||
|
}
|
||||||
|
]
|
||||||
956
package-lock.json
generated
956
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -2,7 +2,6 @@
|
|||||||
"name": "blackroad-operating-system",
|
"name": "blackroad-operating-system",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"main": "server.mjs",
|
"main": "server.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.mjs",
|
"start": "node server.mjs",
|
||||||
@@ -10,21 +9,19 @@
|
|||||||
"build": "tsc --noEmit",
|
"build": "tsc --noEmit",
|
||||||
"lint": "eslint . --ext .ts,.js || true",
|
"lint": "eslint . --ext .ts,.js || true",
|
||||||
"test": "echo \"No automated tests yet\"",
|
"test": "echo \"No automated tests yet\"",
|
||||||
"check:health": "ts-node tools/health-check.ts"
|
"check:health": "ts-node tools/health-check.ts",
|
||||||
|
"deploy:service": "ts-node scripts/deployService.ts",
|
||||||
|
"deploy:all": "ts-node scripts/deployAll.ts",
|
||||||
|
"health:all": "ts-node scripts/checkHealth.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.0"
|
"express": "^4.21.0",
|
||||||
|
"node-fetch": "^2.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/node-fetch": "^2.6.11",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.6.0"
|
||||||
"type": "module",
|
|
||||||
"main": "server.mjs",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node server.mjs"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.21.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
scripts/checkHealth.ts
Normal file
41
scripts/checkHealth.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { ServiceConfig } from "./types";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
function loadServices(): ServiceConfig[] {
|
||||||
|
const filePath = path.join(__dirname, "..", "infra", "services.json");
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return JSON.parse(raw) as ServiceConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkService(service: ServiceConfig) {
|
||||||
|
const url = `https://${service.domain}${service.healthPath}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: "GET" });
|
||||||
|
const statusText = res.statusText || "";
|
||||||
|
console.log(
|
||||||
|
`[OK] ${service.id.padEnd(8)} ${res.status} ${statusText} - ${url}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
`[FAIL] ${service.id.padEnd(8)} - ${(err as Error).message} - ${url}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const services = loadServices();
|
||||||
|
console.log("\n🌡 Checking health of all services:\n");
|
||||||
|
|
||||||
|
for (const svc of services) {
|
||||||
|
await checkService(svc);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nDone.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
69
scripts/deployAll.ts
Normal file
69
scripts/deployAll.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { spawn } from "child_process";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { ServiceConfig } from "./types";
|
||||||
|
|
||||||
|
function loadServices(): ServiceConfig[] {
|
||||||
|
const filePath = path.join(__dirname, "..", "infra", "services.json");
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return JSON.parse(raw) as ServiceConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(cmd: string, args: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: process.platform === "win32"
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Command failed with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployAll() {
|
||||||
|
const services = loadServices();
|
||||||
|
|
||||||
|
// Fixed order so dependencies come up first
|
||||||
|
const order = ["core", "api", "operator", "agents", "console", "web", "docs"];
|
||||||
|
|
||||||
|
const ordered = order
|
||||||
|
.map((id) => services.find((s) => s.id === id))
|
||||||
|
.filter((s): s is ServiceConfig => Boolean(s));
|
||||||
|
|
||||||
|
for (const service of ordered) {
|
||||||
|
console.log(
|
||||||
|
`\n===============================\nDeploying ${service.id} (${service.name})\n===============================`
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"railway",
|
||||||
|
"up",
|
||||||
|
"--project",
|
||||||
|
service.railwayProject,
|
||||||
|
"--service",
|
||||||
|
service.railwayService
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runCommand("npx", args);
|
||||||
|
console.log(`✅ Done: ${service.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed: ${service.id}`);
|
||||||
|
console.error(String(err));
|
||||||
|
// Keep going to try the rest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n🏁 Deployment run finished.");
|
||||||
|
}
|
||||||
|
|
||||||
|
deployAll().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
61
scripts/deployService.ts
Normal file
61
scripts/deployService.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { spawn } from "child_process";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { ServiceConfig } from "./types";
|
||||||
|
|
||||||
|
function loadServices(): ServiceConfig[] {
|
||||||
|
const filePath = path.join(__dirname, "..", "infra", "services.json");
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return JSON.parse(raw) as ServiceConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function deployService(serviceId: string) {
|
||||||
|
const services = loadServices();
|
||||||
|
const service = services.find((s) => s.id === serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
console.error(`Service "${serviceId}" not found.`);
|
||||||
|
console.error(
|
||||||
|
"Valid ids: " + services.map((s) => s.id).join(", ")
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🚀 Deploying service: ${service.name} (${service.id})`);
|
||||||
|
console.log(
|
||||||
|
` Railway project: ${service.railwayProject}, service: ${service.railwayService}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const cmd = "npx";
|
||||||
|
const args = [
|
||||||
|
"railway",
|
||||||
|
"up",
|
||||||
|
"--project",
|
||||||
|
service.railwayProject,
|
||||||
|
"--service",
|
||||||
|
service.railwayService
|
||||||
|
];
|
||||||
|
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: process.platform === "win32"
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
console.log(`✅ Deploy complete: ${service.id}`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Deploy failed for ${service.id} (exit code ${code})`);
|
||||||
|
}
|
||||||
|
process.exit(code === null ? 1 : code);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceId = process.argv[2];
|
||||||
|
|
||||||
|
if (!serviceId) {
|
||||||
|
console.error("Usage: npm run deploy:service -- <serviceId>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
deployService(serviceId);
|
||||||
10
scripts/types.ts
Normal file
10
scripts/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface ServiceConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
repo: string;
|
||||||
|
kind: "backend" | "frontend" | "worker";
|
||||||
|
railwayProject: string;
|
||||||
|
railwayService: string;
|
||||||
|
domain: string;
|
||||||
|
healthPath: string;
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2021",
|
"target": "ES2019",
|
||||||
"module": "CommonJS",
|
"module": "commonjs",
|
||||||
"moduleResolution": "Node",
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["node"],
|
"resolveJsonModule": true,
|
||||||
"lib": ["ES2021", "DOM"]
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"include": ["tools/**/*.ts", "os-spec/**/*.json"]
|
"include": [
|
||||||
|
"scripts/**/*.ts",
|
||||||
|
"infra/**/*.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user