Scaffold core runtime with Railway deployment
Node.js HTTP server with /health, /v1/agents, /v1/agent endpoints. Includes railway.toml, Dockerfile, CI/CD workflows, and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
.github/workflows/ci.yml
vendored
Normal file
19
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
||||||
37
.github/workflows/deploy-railway.yml
vendored
Normal file
37
.github/workflows/deploy-railway.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Railway
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy to Railway
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Railway CLI
|
||||||
|
run: npm install -g @railway/cli
|
||||||
|
|
||||||
|
- name: Deploy to Railway
|
||||||
|
run: railway up --detach
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
|
||||||
|
|
||||||
|
- name: Wait for deployment
|
||||||
|
run: sleep 30
|
||||||
|
|
||||||
|
- name: Health Check
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
HEALTH_URL="${{ secrets.RAILWAY_SERVICE_URL }}/health"
|
||||||
|
if [ -n "${{ secrets.RAILWAY_SERVICE_URL }}" ]; then
|
||||||
|
echo "Checking health at: $HEALTH_URL"
|
||||||
|
curl -f --retry 3 --retry-delay 10 "$HEALTH_URL" || echo "Health check pending"
|
||||||
|
else
|
||||||
|
echo "RAILWAY_SERVICE_URL not set yet, skipping health check"
|
||||||
|
fi
|
||||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["npm", "start"]
|
||||||
35
README.md
35
README.md
@@ -1,2 +1,35 @@
|
|||||||
# blackroad-core
|
# blackroad-core
|
||||||
Core orchestration layer and runtime engine for BlackRoad OS.
|
|
||||||
|
Core orchestration layer and runtime engine for BlackRoad OS.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # Development (auto-reload)
|
||||||
|
npm start # Production
|
||||||
|
npm test # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/health` | Health check |
|
||||||
|
| GET | `/metrics` | Runtime metrics |
|
||||||
|
| GET | `/v1/agents` | Agent roster |
|
||||||
|
| POST | `/v1/agent` | Agent invocation |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deploys to Railway on push to `main`. See `railway.toml` for config.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PORT` | `8080` | Server port |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Proprietary - BlackRoad OS, Inc. All rights reserved.
|
||||||
|
|||||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@blackroad-os-inc/core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Core orchestration layer and runtime engine for BlackRoad OS",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js",
|
||||||
|
"test": "node tests/server.test.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
railway.toml
Normal file
9
railway.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[build]
|
||||||
|
builder = "NIXPACKS"
|
||||||
|
|
||||||
|
[deploy]
|
||||||
|
startCommand = "npm start"
|
||||||
|
healthcheckPath = "/health"
|
||||||
|
healthcheckTimeout = 300
|
||||||
|
restartPolicyType = "ON_FAILURE"
|
||||||
|
restartPolicyMaxRetries = 10
|
||||||
77
server.js
Normal file
77
server.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const http = require('http')
|
||||||
|
const { randomUUID } = require('crypto')
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT) || 8080
|
||||||
|
const VERSION = '0.1.0'
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
totalRequests: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
snapshot() {
|
||||||
|
return {
|
||||||
|
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
|
||||||
|
total_requests: this.totalRequests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
metrics.totalRequests++
|
||||||
|
const requestId = randomUUID()
|
||||||
|
|
||||||
|
const send = (code, body) => {
|
||||||
|
res.writeHead(code, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && (req.url === '/health' || req.url === '/healthz')) {
|
||||||
|
return send(200, {
|
||||||
|
status: 'ok',
|
||||||
|
service: 'blackroad-core',
|
||||||
|
version: VERSION,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && req.url === '/metrics') {
|
||||||
|
return send(200, { status: 'ok', metrics: metrics.snapshot() })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && req.url === '/v1/agents') {
|
||||||
|
return send(200, { status: 'ok', agents: [], request_id: requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST' && req.url === '/v1/agent') {
|
||||||
|
let body = ''
|
||||||
|
req.on('data', (chunk) => { body += chunk })
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(body)
|
||||||
|
if (!payload.agent || !payload.intent || typeof payload.input !== 'string') {
|
||||||
|
return send(400, { status: 'error', error: 'Missing agent, intent, or input', request_id: requestId })
|
||||||
|
}
|
||||||
|
return send(200, {
|
||||||
|
status: 'ok',
|
||||||
|
output: '',
|
||||||
|
request_id: requestId,
|
||||||
|
message: 'Agent invocation not yet connected to providers'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return send(400, { status: 'error', error: 'Invalid JSON', request_id: requestId })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
send(404, { status: 'error', error: 'Not found', request_id: requestId })
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`BlackRoad Core v${VERSION} listening on 0.0.0.0:${PORT}`)
|
||||||
|
console.log(' POST /v1/agent - Agent invocation')
|
||||||
|
console.log(' GET /v1/agents - Agent roster')
|
||||||
|
console.log(' GET /health - Health check')
|
||||||
|
console.log(' GET /metrics - Metrics')
|
||||||
|
})
|
||||||
81
tests/server.test.js
Normal file
81
tests/server.test.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const http = require('http')
|
||||||
|
const { spawn } = require('child_process')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const PORT = 9876
|
||||||
|
let serverProcess
|
||||||
|
|
||||||
|
function request(method, urlPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({ hostname: '127.0.0.1', port: PORT, path: urlPath, method }, (res) => {
|
||||||
|
let body = ''
|
||||||
|
res.on('data', (chunk) => { body += chunk })
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode, body: JSON.parse(body) })
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode, body })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
req.on('error', reject)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
let passed = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(` PASS: ${message}`)
|
||||||
|
passed++
|
||||||
|
} else {
|
||||||
|
console.error(` FAIL: ${message}`)
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting server...')
|
||||||
|
serverProcess = spawn('node', [path.join(__dirname, '..', 'server.js')], {
|
||||||
|
env: { ...process.env, PORT: String(PORT) },
|
||||||
|
stdio: 'pipe'
|
||||||
|
})
|
||||||
|
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
console.log('\nRunning tests:\n')
|
||||||
|
|
||||||
|
const health = await request('GET', '/health')
|
||||||
|
assert(health.status === 200, '/health returns 200')
|
||||||
|
assert(health.body.status === 'ok', '/health status is ok')
|
||||||
|
assert(health.body.service === 'blackroad-core', '/health service name correct')
|
||||||
|
|
||||||
|
const agents = await request('GET', '/v1/agents')
|
||||||
|
assert(agents.status === 200, '/v1/agents returns 200')
|
||||||
|
assert(Array.isArray(agents.body.agents), '/v1/agents returns array')
|
||||||
|
|
||||||
|
const metrics = await request('GET', '/metrics')
|
||||||
|
assert(metrics.status === 200, '/metrics returns 200')
|
||||||
|
|
||||||
|
const notFound = await request('GET', '/nonexistent')
|
||||||
|
assert(notFound.status === 404, 'Unknown route returns 404')
|
||||||
|
|
||||||
|
console.log(`\nResults: ${passed} passed, ${failed} failed`)
|
||||||
|
|
||||||
|
serverProcess.kill()
|
||||||
|
process.exit(failed > 0 ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
if (serverProcess) serverProcess.kill()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user