Merge commit 'ada9006b1d2e73bc72267f4a7e6c34d5fb333b12'

This commit is contained in:
Alexa Amundson
2025-11-21 00:09:57 -06:00
8 changed files with 430 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
name: Deploy All Services name: Deploy All Services
name: Deploy BlackRoad OS
on: on:
workflow_dispatch: workflow_dispatch:
@@ -7,6 +8,9 @@ on:
description: "Optional service id to deploy" description: "Optional service id to deploy"
required: false required: false
type: string type: string
description: "Optional service id to deploy (core, api, operator, agents, console, web, docs)"
required: false
default: ""
push: push:
branches: branches:
- main - main
@@ -25,6 +29,14 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Use Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@@ -34,5 +46,14 @@ jobs:
if [ -n "${{ github.event.inputs.serviceId }}" ]; then if [ -n "${{ github.event.inputs.serviceId }}" ]; then
npm run deploy:service -- ${{ github.event.inputs.serviceId }} npm run deploy:service -- ${{ github.event.inputs.serviceId }}
else else
- 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 npm run deploy:all
fi fi

View File

@@ -27,3 +27,39 @@ Use the **Deploy All** workflow in the Actions tab (`.github/workflows/deploy-al
- Trigger manually with **Run workflow** to deploy every service. - Trigger manually with **Run workflow** to deploy every service.
- Provide the optional `serviceId` input to deploy just one service using `deploy:service`. - Provide the optional `serviceId` input to deploy just one service using `deploy:service`.
- Deployments expect `RAILWAY_TOKEN` to be available as a GitHub Actions secret and run with `NODE_ENV=production`. - Deployments expect `RAILWAY_TOKEN` to be available as a GitHub Actions secret and run with `NODE_ENV=production`.
# 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.

185
package-lock.json generated
View File

@@ -8,13 +8,17 @@
"name": "blackroad-operating-system", "name": "blackroad-operating-system",
"version": "1.0.0", "version": "1.0.0",
"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",
"node-fetch": "^3.0.0", "node-fetch": "^3.0.0",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.0.0" "typescript": "^5.0.0"
"@types/node-fetch": "^2.6.11",
"ts-node": "^10.9.2",
"typescript": "^5.6.0"
} }
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -94,6 +98,65 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@@ -148,6 +211,13 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -210,6 +280,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -272,6 +355,16 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -360,6 +453,22 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -474,6 +583,21 @@
}, },
"engines": { "engines": {
"node": ">=12.20.0" "node": ">=12.20.0"
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/forwarded": { "node_modules/forwarded": {
@@ -564,6 +688,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -748,6 +888,24 @@
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/node-fetch" "url": "https://opencollective.com/node-fetch"
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
@@ -1017,6 +1175,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-node": { "node_modules/ts-node": {
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -1137,6 +1301,25 @@
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/yn": { "node_modules/yn": {

View File

@@ -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",
@@ -14,14 +13,22 @@
"deploy:service": "ts-node --esm scripts/deployService.ts", "deploy:service": "ts-node --esm scripts/deployService.ts",
"deploy:all": "ts-node --esm scripts/deployAll.ts", "deploy:all": "ts-node --esm scripts/deployAll.ts",
"health:all": "ts-node --esm scripts/checkHealth.ts" "health:all": "ts-node --esm scripts/checkHealth.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",
"node-fetch": "^3.0.0", "node-fetch": "^3.0.0",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.0.0" "typescript": "^5.0.0"
"@types/node-fetch": "^2.6.11",
"ts-node": "^10.9.2",
"typescript": "^5.6.0"
} }
} }

View File

@@ -33,3 +33,44 @@ async function main(): Promise<void> {
} }
main(); main();
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);
});

View File

@@ -30,3 +30,72 @@ async function main(): Promise<void> {
} }
main(); main();
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);
});

View File

@@ -84,3 +84,63 @@ async function main(): Promise<void> {
} }
main(); main();
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);

View File

@@ -3,14 +3,21 @@
"target": "ES2021", "target": "ES2021",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"target": "ES2019",
"module": "commonjs",
"strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"], "resolveJsonModule": true,
"lib": ["ES2021", "DOM"] "outDir": "dist",
"rootDir": "."
}, },
"include": ["scripts/**/*.ts", "tools/**/*.ts", "infra/**/*.json"], "include": ["scripts/**/*.ts", "tools/**/*.ts", "infra/**/*.json"],
"ts-node": { "ts-node": {
"esm": true "esm": true
} }
"include": [
"scripts/**/*.ts",
"infra/**/*.json"
]
} }