add railway secrets automation

This commit is contained in:
Alexa Amundson
2025-11-16 03:48:34 -06:00
parent 84ab793177
commit 9b4d04523f
5 changed files with 365 additions and 59 deletions

View File

@@ -0,0 +1,48 @@
name: Railway Secrets & Automation Audit
on:
push:
branches: ["main", "claude/**"]
pull_request:
branches: ["main"]
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
jobs:
validate:
name: Validate Railway configuration
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Run Railway validation script
run: |
python scripts/railway/validate_env_template.py
summary:
name: Automation summary
runs-on: ubuntu-latest
needs: validate
if: always()
steps:
- name: Summarize results
run: |
echo ""
echo "╔════════════════════════════════════════════════════════╗"
if [ "${{ needs.validate.result }}" = "success" ]; then
echo "║ ✅ Railway secrets automation checks succeeded ║"
else
echo "║ ❌ Railway automation detected configuration drift ║"
fi
echo "║ scripts/railway/validate_env_template.py ║"
echo "║ keeps .env.example and Railway config synced ║"
echo "╚════════════════════════════════════════════════════════╝"

View File

@@ -82,6 +82,20 @@ The GitHub Pages workflow publishes the canonical frontend from
`backend/static/index.html` (and any supporting assets in that directory) so `backend/static/index.html` (and any supporting assets in that directory) so
the validation and deploy jobs keep pointing at the same file. the validation and deploy jobs keep pointing at the same file.
### Railway Secrets & Automation
- **Vercel-free deploys** the backend ships with a Railway-native workflow
(`Deploy to Railway`) so you can ignore Vercel entirely.
- **Single source of truth** `backend/.env.example` enumerates every runtime
variable and uses obvious placeholders so you can paste the file into the
Railway variables dashboard without leaking credentials.
- **CI enforcement** the GitHub Action `Railway Secrets & Automation Audit`
runs `scripts/railway/validate_env_template.py` on every PR + nightly to make
sure the template, `railway.toml`, and `railway.json` never drift from the
FastAPI settings.
- **Manual check** run `python scripts/railway/validate_env_template.py`
locally to get the same assurance before pushing.
## Architecture ## Architecture
### Single-Page Application ### Single-Page Application

View File

@@ -1,83 +1,76 @@
# Database # BlackRoad OS Backend - Railway Secrets Template
DATABASE_URL=postgresql://blackroad:password@localhost:5432/blackroad_db # Copy this file to .env for local development and keep the actual
DATABASE_ASYNC_URL=postgresql+asyncpg://blackroad:password@localhost:5432/blackroad_db # values in Railway's Variables dashboard. The GitHub workflow
# scripts/railway/validate_env_template.py ensures this template stays
# aligned with app/app.config.Settings.
# Redis # Application metadata
REDIS_URL=redis://localhost:6379/0 APP_NAME=BlackRoad Operating System
APP_VERSION=1.0.0
DEBUG=False
ENVIRONMENT=production
# JWT # Database connectivity
SECRET_KEY=your-secret-key-change-this-in-production DATABASE_URL=postgresql://YOUR_DB_USER:YOUR_DB_PASSWORD@YOUR_DB_HOST:5432/blackroad
DATABASE_ASYNC_URL=postgresql+asyncpg://YOUR_DB_USER:YOUR_DB_PASSWORD@YOUR_DB_HOST:5432/blackroad
REDIS_URL=redis://YOUR_REDIS_HOST:6379/0
# Security / auth
SECRET_KEY=changeme-super-secret-key
ALGORITHM=HS256 ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30 ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7 REFRESH_TOKEN_EXPIRE_DAYS=7
WALLET_MASTER_KEY=replace-with-strong-unique-master-key WALLET_MASTER_KEY=changeme-wallet-master-key
ALLOWED_ORIGINS=https://blackroad.systems,https://your-frontend.example
# AWS S3 (for file storage) # Object storage
AWS_ACCESS_KEY_ID=your-access-key AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=your-secret-key AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
AWS_REGION=us-east-1 AWS_REGION=us-east-1
S3_BUCKET_NAME=blackroad-files S3_BUCKET_NAME=blackroad-files
# Email (SMTP) # Email / SMTP
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_USER=your-email@gmail.com SMTP_USER=road@example.com
SMTP_PASSWORD=your-app-password SMTP_PASSWORD=changeme-smtp-password
EMAIL_FROM=noreply@blackroad.com EMAIL_FROM=blackroad@example.com
# Application # AI integrations
APP_NAME=BlackRoad Operating System OPENAI_API_KEY=sk-your-openai-key
APP_VERSION=1.0.0
DEBUG=True
ENVIRONMENT=development
# CORS (add your production domains here) # Blockchain tuning
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://blackboxprogramming.github.io,https://www.blackroad.systems BLOCKCHAIN_DIFFICULTY=4
MINING_REWARD=50.0
# API Keys - Existing Integrations # Railway deployment + alerting
OPENAI_API_KEY=your-openai-key-for-ai-chat RAILWAY_TOKEN=railway-token-placeholder
RAILWAY_PROJECT_ID=00000000-0000-0000-0000-000000000000
RAILWAY_ENVIRONMENT_ID=00000000-0000-0000-0000-000000000000
RAILWAY_DOMAIN=your-service.up.railway.app
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/XXX/YYY
# Optional cloud/API integrations
DIGITALOCEAN_TOKEN=your-digitalocean-token DIGITALOCEAN_TOKEN=your-digitalocean-token
GITHUB_TOKEN=your-github-personal-access-token GITHUB_TOKEN=your-github-personal-access-token
HUGGINGFACE_TOKEN=your-huggingface-api-token HUGGINGFACE_TOKEN=your-huggingface-token
VERCEL_TOKEN=vercel-token-placeholder
# API Keys - New Deployment Platform Integrations VERCEL_TEAM_ID=your-vercel-team-id
RAILWAY_TOKEN=your-railway-api-token
VERCEL_TOKEN=your-vercel-api-token
VERCEL_TEAM_ID=your-vercel-team-id-optional
# API Keys - Payment Processing
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
STRIPE_PUBLISHABLE_KEY=pk_test_your-stripe-publishable-key STRIPE_PUBLISHABLE_KEY=pk_test_your-stripe-publishable-key
TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# API Keys - Communications (SMS/WhatsApp)
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=+1234567890 TWILIO_PHONE_NUMBER=+10000000000
# API Keys - Team Collaboration & Notifications
SLACK_BOT_TOKEN=xoxb-your-slack-bot-token SLACK_BOT_TOKEN=xoxb-your-slack-bot-token
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL DISCORD_BOT_TOKEN=discord-bot-token-placeholder
DISCORD_BOT_TOKEN=your-discord-bot-token SENTRY_DSN=https://example.ingest.sentry.io/project-id
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR/WEBHOOK/URL
# API Keys - Error Tracking & Monitoring
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_AUTH_TOKEN=your-sentry-auth-token SENTRY_AUTH_TOKEN=your-sentry-auth-token
SENTRY_ORG=your-sentry-organization-slug SENTRY_ORG=your-sentry-org
ROADCHAIN_RPC_URL=https://chain.example-rpc.net
# Blockchain & Mining ROADCOIN_POOL_URL=pool.example.roadcoin:3333
BLOCKCHAIN_DIFFICULTY=4 ROADCOIN_WALLET_ADDRESS=your-roadcoin-wallet
MINING_REWARD=50 MQTT_BROKER_URL=mqtt://broker.example.internal:1883
ROADCHAIN_RPC_URL=http://localhost:8545
ROADCOIN_POOL_URL=pool.roadcoin.network:3333
ROADCOIN_WALLET_ADDRESS=auto-generated-per-user
# Device Management (IoT/Raspberry Pi)
MQTT_BROKER_URL=mqtt://localhost:1883
MQTT_USERNAME=blackroad MQTT_USERNAME=blackroad
MQTT_PASSWORD=your-mqtt-password MQTT_PASSWORD=your-mqtt-password
DEVICE_HEARTBEAT_TIMEOUT_SECONDS=300 DEVICE_HEARTBEAT_TIMEOUT_SECONDS=300
# Deployment
# Railway will automatically set PORT - use this for local development
PORT=8000

View File

@@ -26,6 +26,28 @@ SECRET_KEY=your-secret-key
ALLOWED_ORIGINS=https://yourdomain.com ALLOWED_ORIGINS=https://yourdomain.com
``` ```
> 💡 **Railway-first secrets**: copy `backend/.env.example` to `.env` for local
> runs, but store the real values inside your Railway project. The template is
> kept in sync with `app.config.Settings` by the
> [`scripts/railway/validate_env_template.py`](../scripts/railway/validate_env_template.py)
> audit script, which also runs automatically in CI.
### Railway Secrets Checklist
1. Install the Railway CLI and authenticate: `curl -fsSL https://railway.app/install.sh | sh`
2. Link the project locally: `railway link <project-id>`
3. Use the template to populate secrets:
```bash
while IFS='=' read -r key value; do
if [[ -n "$key" && $key != \#* ]]; then
railway variables set "$key" "$value"
fi
done < backend/.env.example
```
4. Override placeholder values using the Railway dashboard or `railway variables set`.
5. Verify everything with `railway variables list` or by running the GitHub
Action **Railway Secrets & Automation Audit** from the Actions tab.
## Local Development ## Local Development
### 1. Setup Virtual Environment ### 1. Setup Virtual Environment

View File

@@ -0,0 +1,229 @@
"""Validate Railway env template and deployment config."""
from __future__ import annotations
import argparse
import ast
import json
from pathlib import Path
from typing import Dict, List, Set, Tuple
import tomllib
REPO_ROOT = Path(__file__).resolve().parents[2]
BACKEND_DIR = REPO_ROOT / "backend"
CONFIG_PATH = BACKEND_DIR / "app" / "config.py"
ENV_TEMPLATE_PATH = BACKEND_DIR / ".env.example"
RAILWAY_TOML_PATH = REPO_ROOT / "railway.toml"
RAILWAY_JSON_PATH = REPO_ROOT / "railway.json"
# Keys that live outside app.config.Settings but should exist for automation
EXTRA_REQUIRED_KEYS: Set[str] = {
"RAILWAY_TOKEN",
"RAILWAY_PROJECT_ID",
"RAILWAY_ENVIRONMENT_ID",
"RAILWAY_DOMAIN",
"SLACK_WEBHOOK_URL",
"DISCORD_WEBHOOK_URL",
"DIGITALOCEAN_TOKEN",
"GITHUB_TOKEN",
"HUGGINGFACE_TOKEN",
"VERCEL_TOKEN",
"VERCEL_TEAM_ID",
"STRIPE_SECRET_KEY",
"STRIPE_PUBLISHABLE_KEY",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
"TWILIO_PHONE_NUMBER",
"SLACK_BOT_TOKEN",
"DISCORD_BOT_TOKEN",
"SENTRY_DSN",
"SENTRY_AUTH_TOKEN",
"SENTRY_ORG",
"ROADCHAIN_RPC_URL",
"ROADCOIN_POOL_URL",
"ROADCOIN_WALLET_ADDRESS",
"MQTT_BROKER_URL",
"MQTT_USERNAME",
"MQTT_PASSWORD",
"DEVICE_HEARTBEAT_TIMEOUT_SECONDS",
}
SENSITIVE_KEYS: Set[str] = {
"DATABASE_URL",
"DATABASE_ASYNC_URL",
"REDIS_URL",
"SECRET_KEY",
"WALLET_MASTER_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"SMTP_PASSWORD",
"OPENAI_API_KEY",
"RAILWAY_TOKEN",
"RAILWAY_PROJECT_ID",
"RAILWAY_ENVIRONMENT_ID",
"RAILWAY_DOMAIN",
"SLACK_WEBHOOK_URL",
"DISCORD_WEBHOOK_URL",
"DIGITALOCEAN_TOKEN",
"GITHUB_TOKEN",
"HUGGINGFACE_TOKEN",
"VERCEL_TOKEN",
"STRIPE_SECRET_KEY",
"STRIPE_PUBLISHABLE_KEY",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
"TWILIO_PHONE_NUMBER",
"SLACK_BOT_TOKEN",
"DISCORD_BOT_TOKEN",
"SENTRY_DSN",
"SENTRY_AUTH_TOKEN",
"ROADCHAIN_RPC_URL",
"ROADCOIN_POOL_URL",
"ROADCOIN_WALLET_ADDRESS",
"MQTT_BROKER_URL",
"MQTT_PASSWORD",
}
PLACEHOLDER_MARKERS: Tuple[str, ...] = (
"changeme",
"your_",
"your-",
"placeholder",
"example",
"dummy",
"xxxx",
"xxx",
"yyy",
"zzz",
"0000",
)
def parse_env_template(path: Path) -> Dict[str, str]:
env: Dict[str, str] = {}
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
env[key.strip()] = value.strip()
return env
def extract_settings_fields(config_path: Path) -> List[str]:
tree = ast.parse(config_path.read_text())
fields: Set[str] = set()
class SettingsVisitor(ast.NodeVisitor):
def visit_ClassDef(self, node: ast.ClassDef) -> None:
if node.name != "Settings":
return
for stmt in node.body:
if isinstance(stmt, ast.ClassDef):
continue
if isinstance(stmt, ast.Assign):
for target in stmt.targets:
if isinstance(target, ast.Name):
fields.add(target.id)
elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
fields.add(stmt.target.id)
SettingsVisitor().visit(tree)
return sorted(fields)
def is_placeholder(value: str) -> bool:
lower_value = value.lower()
return any(marker in lower_value for marker in PLACEHOLDER_MARKERS)
def validate_env_template() -> None:
if not ENV_TEMPLATE_PATH.exists():
raise SystemExit(".env.example template is missing")
env_values = parse_env_template(ENV_TEMPLATE_PATH)
settings_fields = extract_settings_fields(CONFIG_PATH)
required_keys = set(settings_fields) | EXTRA_REQUIRED_KEYS
missing = sorted(key for key in required_keys if key not in env_values)
if missing:
raise SystemExit(
"Missing keys in .env.example: " + ", ".join(missing)
)
unexpected = sorted(
key for key in env_values.keys() if key not in required_keys
)
if unexpected:
print("⚠️ Found extra keys in .env.example:", ", ".join(unexpected))
insecure = sorted(
key
for key in SENSITIVE_KEYS
if key in env_values and not is_placeholder(env_values[key])
)
if insecure:
raise SystemExit(
"Sensitive keys must use placeholders: " + ", ".join(insecure)
)
print("✅ .env.example matches app.config.Settings")
def validate_railway_configs() -> None:
toml_data = tomllib.loads(RAILWAY_TOML_PATH.read_text())
json_data = json.loads(RAILWAY_JSON_PATH.read_text()) if RAILWAY_JSON_PATH.exists() else {}
build = toml_data.get("build", {})
if build.get("builder") != "DOCKERFILE":
raise SystemExit("railway.toml must use the Dockerfile builder")
if build.get("dockerfilePath") != "backend/Dockerfile":
raise SystemExit("railway.toml dockerfilePath must be backend/Dockerfile")
deploy = toml_data.get("deploy", {})
start_command = deploy.get("startCommand", "")
if "$PORT" not in start_command:
raise SystemExit("Railway start command must forward the $PORT value")
services = toml_data.get("services", [])
env_names = {
env_entry.get("name")
for service in services
if isinstance(service, dict)
for env_entry in service.get("env", [])
if isinstance(env_entry, dict)
}
for required in ("ENVIRONMENT", "DEBUG"):
if required not in env_names:
raise SystemExit(f"Railway services must set {required}")
json_build = json_data.get("build", {})
if json_build.get("builder") and json_build.get("builder") != "DOCKERFILE":
raise SystemExit("railway.json builder must remain DOCKERFILE")
json_path = json_build.get("dockerfilePath")
if json_path and json_path != "backend/Dockerfile":
raise SystemExit("railway.json dockerfilePath must match backend/Dockerfile")
print("✅ Railway deployment descriptors look consistent")
def main() -> None:
parser = argparse.ArgumentParser(description="Validate Railway secret automation")
parser.add_argument("--skip-env", action="store_true", help="Skip env template validation")
parser.add_argument("--skip-config", action="store_true", help="Skip Railway config validation")
args = parser.parse_args()
if not args.skip_env:
validate_env_template()
if not args.skip_config:
validate_railway_configs()
print("\nAll Railway automation checks passed ✅")
if __name__ == "__main__":
main()