diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03e8dec..74b639a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' @@ -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 @@ -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: | @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a3f8a31 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 617ac18..fef220d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/monitoring/tests/alert-rules.test.yml b/monitoring/tests/alert-rules.test.yml new file mode 100644 index 0000000..66fcb0c --- /dev/null +++ b/monitoring/tests/alert-rules.test.yml @@ -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: [] diff --git a/package.json b/package.json index 7f5b271..e8a857b 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } } diff --git a/scripts/06-smoke-test.js b/scripts/06-smoke-test.js index f0078af..1c3c645 100644 --- a/scripts/06-smoke-test.js +++ b/scripts/06-smoke-test.js @@ -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; @@ -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");