Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ env:
KUBE_LINTER_VERSION: '0.7.1'
PROMETHEUS_VERSION: '2.53.2'
ALERTMANAGER_VERSION: '0.27.0'
LYCHEE_VERSION: '0.15.1'
GITLEAKS_VERSION: '8.18.4'
TRIVY_VERSION: '0.55.2'
HADOLINT_VERSION: '2.12.0'

Expand All @@ -37,6 +39,32 @@ jobs:
# actionlint also runs shellcheck on every `run:` block.
run: actionlint -color

secrets:
name: Scan for secrets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install gitleaks
run: |
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
- name: Run gitleaks
run: gitleaks detect --source . --redact --verbose

links:
name: Check Markdown links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install lychee
run: |
curl -sSfL "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" \
| tar -xz -C /usr/local/bin lychee
- name: Run lychee
run: lychee --no-progress --verbose README.md 'docs/**/*.md'

shell:
name: Lint shell scripts
runs-on: ubuntu-latest
Expand Down Expand Up @@ -122,6 +150,8 @@ jobs:
| tar -xz --strip-components=1 -C /usr/local/bin "alertmanager-${ALERTMANAGER_VERSION}.linux-amd64/amtool"
- name: Check alert rules
run: promtool check rules monitoring/alert-rules.yml
- name: Test alert rules
run: promtool test rules monitoring/tests/*.yml
- name: Check Prometheus config
# prometheus.yml references the rules at their in-container path.
run: |
Expand All @@ -144,6 +174,39 @@ jobs:
cp .env.example .env
docker compose config --quiet

compose-smoke:
name: Smoke-test docker-compose startup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Checkout application source
uses: actions/checkout@v7
with:
repository: SynapsVault/backend
path: src/backend
- name: Checkout frontend source
uses: actions/checkout@v7
with:
repository: SynapsVault/frontend
path: src/frontend
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- name: Install dependencies
run: npm ci
- name: Prepare env
run: cp .env.example .env
- name: Start stack
run: docker compose up -d --wait
- name: Run smoke test
run: node scripts/06-smoke-test.js
- name: Dump logs on failure
if: failure()
run: docker compose logs --no-color
- name: Tear down
if: always()
run: docker compose down -v
hadolint:
name: Lint Dockerfiles
runs-on: ubuntu-latest
Expand Down
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Pre-commit hooks for the Stellar Soroban Playground.
# Install with: pip install pre-commit && pre-commit install
# Run manually against all files: pre-commit run --all-files
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
name: gitleaks (detect hardcoded secrets)
description: Scan staged changes for Stellar secret keys, DB URLs, and other credentials.
entry: gitleaks protect --verbose --redact --staged
language: golang
pass_filenames: false
36 changes: 33 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ services:
condition: service_healthy
migrate:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- app_network

Expand All @@ -95,7 +101,13 @@ services:
- "${FRONTEND_PORT:-8080}:80"
depends_on:
backend:
condition: service_started
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
networks:
- app_network

Expand Down Expand Up @@ -131,7 +143,13 @@ services:
- "${PROMETHEUS_PORT:-9090}:9090"
depends_on:
backend:
condition: service_started
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:9090/-/healthy || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
networks:
- app_network

Expand All @@ -149,6 +167,12 @@ services:
- "--storage.path=/alertmanager"
ports:
- "${ALERTMANAGER_PORT:-9093}:9093"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:9093/-/healthy || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
networks:
- app_network

Expand All @@ -170,7 +194,13 @@ services:
- "${GRAFANA_PORT:-3001}:3000"
depends_on:
prometheus:
condition: service_started
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- app_network

Expand Down
87 changes: 87 additions & 0 deletions monitoring/tests/alert-rules.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
rule_files:
- ../alert-rules.yml

evaluation_interval: 1m

tests:
# BackendDown: backend target is down for longer than the alert's `for` window.
- interval: 1m
input_series:
- series: 'up{job="backend",instance="backend:8080"}'
values: '0x10'
alert_rule_test:
- eval_time: 10m
alertname: BackendDown
exp_alerts:
- exp_labels:
severity: critical
job: backend
instance: backend:8080
exp_annotations:
summary: Backend is down
description: Backend target backend:8080 has been down for more than 5 minutes.

# BackendDown must not fire while the backend is up.
- interval: 1m
input_series:
- series: 'up{job="backend",instance="backend:8080"}'
values: '1x10'
alert_rule_test:
- eval_time: 10m
alertname: BackendDown
exp_alerts: []

# High 5xx rate: >5% of requests returning 5xx over the rate window.
- interval: 1m
input_series:
- series: 'http_requests_total{job="backend",status="500"}'
values: '0+60x10'
- series: 'http_requests_total{job="backend",status="200"}'
values: '0+540x10'
alert_rule_test:
- eval_time: 10m
alertname: HighErrorRate
exp_alerts:
- exp_labels:
severity: warning
job: backend
exp_annotations:
summary: High 5xx error rate
description: More than 5% of requests to backend are failing with 5xx over the last 5 minutes.

# High 5xx rate must not fire when the error ratio is low.
- interval: 1m
input_series:
- series: 'http_requests_total{job="backend",status="500"}'
values: '0+1x10'
- series: 'http_requests_total{job="backend",status="200"}'
values: '0+999x10'
alert_rule_test:
- eval_time: 10m
alertname: HighErrorRate
exp_alerts: []

# BackupStale: last successful backup is older than the staleness threshold.
- interval: 1m
input_series:
- series: 'backup_last_success_timestamp_seconds'
values: '0x10'
alert_rule_test:
- eval_time: 10m
alertname: BackupStale
exp_alerts:
- exp_labels:
severity: critical
exp_annotations:
summary: Backup is stale
description: No successful backup has completed in the last 24 hours.

# BackupStale must not fire when a recent backup exists.
- interval: 1m
input_series:
- series: 'backup_last_success_timestamp_seconds'
values: 'stale'
alert_rule_test:
- eval_time: 10m
alertname: BackupStale
exp_alerts: []
24 changes: 6 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "synapsvault-deploy",
"version": "1.0.1",
"type": "module",
"description": "SynapsVault deployment scripts and SCF submission docs",
"name": "handsoff",
"version": "0.1.0",
"private": true,
"scripts": {
"check:links": "markdown-link-check -q -p README.md && markdown-link-check -q -r docs"
"step1": "node scripts/01-generate-keypairs.js",
"step2": "node scripts/02-fund-accounts.js",
"step3": "bash scripts/03-deploy-contracts.sh",
Expand All @@ -18,19 +18,7 @@
"test": "node --test \"test/**/*.test.cjs\"",
"test:smoke": "node scripts/06-smoke-test.js"
},
"keywords": [
"stellar",
"soroban",
"deployment",
"testnet",
"x402"
],
"author": "SynapsVault Team",
"license": "MIT",
"engines": {
"node": ">=22.12.0"
},
"dependencies": {
"@stellar/stellar-sdk": "^17.2.1"
"devDependencies": {
"markdown-link-check": "^3.12.0"
}
}
33 changes: 33 additions & 0 deletions scripts/06-smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
const BASE = process.argv[2] ?? process.env.BACKEND_URL ?? "http://localhost:3000";
const ADMIN_KEY = process.env.ADMIN_API_KEY ?? "change_me";

// When run against a freshly started docker-compose stack, services may still
// be warming up even after `docker compose up -d --wait`. Allow a bounded
// retry window so CI smoke tests are not flaky on cold starts.
const READY_TIMEOUT_MS = Number(process.env.SMOKE_READY_TIMEOUT_MS ?? 60000);
const READY_INTERVAL_MS = 2000;

let passed = 0;
let failed = 0;

Expand All @@ -41,8 +47,35 @@ async function get(path, headers = {}) {
return res.json();
}

async function waitForBackend() {
const deadline = Date.now() + READY_TIMEOUT_MS;
let lastErr;
while (Date.now() < deadline) {
try {
const res = await fetch(`${BASE}/health`);
if (res.ok) return;
lastErr = new Error(`HTTP ${res.status}`);
} catch (err) {
lastErr = err;
}
await new Promise((r) => setTimeout(r, READY_INTERVAL_MS));
}
throw new Error(
`Backend not ready at ${BASE}/health within ${READY_TIMEOUT_MS}ms` +
(lastErr ? ` (last error: ${lastErr.message})` : "")
);
}

console.log(`\n▸ SynapsVault smoke test — ${BASE}\n`);

// Wait for the stack to accept traffic before running checks.
try {
await waitForBackend();
} catch (err) {
console.error(` ❌ ${err.message}`);
process.exit(1);
}

// Health
await check("Backend health", async () => {
const d = await get("/health");
Expand Down