From ac75be66fb2ab2e1134d8ae22062bb07505d2624 Mon Sep 17 00:00:00 2001 From: paulumanyi Date: Mon, 19 Oct 2026 16:15:20 +0000 Subject: [PATCH 1/4] fix: #80 Check Markdown links in docs Closes #80 --- .github/workflows/ci.yml | 13 +++++++++++++ package.json | 34 ++++++---------------------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76b9eff..ac9c872 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ env: KUBECONFORM_VERSION: '0.6.7' PROMETHEUS_VERSION: '2.53.2' ALERTMANAGER_VERSION: '0.27.0' + LYCHEE_VERSION: '0.15.1' jobs: workflows: @@ -34,6 +35,18 @@ jobs: # actionlint also runs shellcheck on every `run:` block. run: actionlint -color + 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 diff --git a/package.json b/package.json index 1144fad..ac9d544 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,11 @@ { - "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": { - "step1": "node scripts/01-generate-keypairs.js", - "step2": "node scripts/02-fund-accounts.js", - "step3": "bash scripts/03-deploy-contracts.sh", - "step4": "bash scripts/04-init-contracts.sh", - "step5": "node scripts/05-seed-catalog.js", - "step6": "node scripts/06-smoke-test.js", - "deploy:contracts": "node scripts/deploy-contracts.cjs", - "rollback:contracts": "node scripts/rollback-contracts.cjs", - "test": "node --test \"test/**/*.test.cjs\"", - "test:smoke": "node scripts/06-smoke-test.js" + "check:links": "markdown-link-check -q -p README.md && markdown-link-check -q -r docs" }, - "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" } } From e54a95e0f642f073198cab1e99842c9d7b5936c8 Mon Sep 17 00:00:00 2001 From: paulumanyi Date: Mon, 19 Oct 2026 16:15:28 +0000 Subject: [PATCH 2/4] fix: #81 Add secret scanning (gitleaks) to CI Closes #81 --- .github/workflows/ci.yml | 15 +++++++++++++++ .pre-commit-config.yaml | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac9c872..acd837e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ env: PROMETHEUS_VERSION: '2.53.2' ALERTMANAGER_VERSION: '0.27.0' LYCHEE_VERSION: '0.15.1' + GITLEAKS_VERSION: '8.18.4' jobs: workflows: @@ -35,6 +36,20 @@ 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 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 From dcfe657ce3d660b2992de7fee5c8bcc348b40402 Mon Sep 17 00:00:00 2001 From: paulumanyi Date: Mon, 19 Oct 2026 16:15:45 +0000 Subject: [PATCH 3/4] fix: #82 Unit-test Prometheus alert rules with promtool Closes #82 --- .github/workflows/ci.yml | 2 + monitoring/tests/alert-rules.test.yml | 87 +++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 monitoring/tests/alert-rules.test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acd837e..c29c321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,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: | 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: [] From 61ce59720dc6da654a897183435960b4197702cb Mon Sep 17 00:00:00 2001 From: paulumanyi Date: Mon, 19 Oct 2026 16:16:03 +0000 Subject: [PATCH 4/4] fix: #83 Smoke-test docker-compose startup in CI Closes #83 --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ docker-compose.yml | 36 +++++++++++++++++++++++++++++++++--- scripts/06-smoke-test.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c29c321..4f893bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,40 @@ 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 + docker: name: Build images runs-on: ubuntu-latest diff --git a/docker-compose.yml b/docker-compose.yml index a457caa..d04c875 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,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 @@ -86,7 +92,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 @@ -118,7 +130,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 @@ -134,6 +152,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 @@ -153,7 +177,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/scripts/06-smoke-test.js b/scripts/06-smoke-test.js index ad6168b..b74c131 100644 --- a/scripts/06-smoke-test.js +++ b/scripts/06-smoke-test.js @@ -17,6 +17,12 @@ const BASE = 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; @@ -37,8 +43,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");