Files
solopm-server/plans/11-integrations.md
T

3.7 KiB

11 — Integrations: Incoming Webhooks, Outgoing Webhooks, Export

Incoming webhooks (GitHub & Gitea)

  • Endpoint POST /webhooks/incoming/{provider} (github|gitea), outside /api/v1, unauthenticated but HMAC-verified
  • Signature: X-Hub-Signature-256: sha256=<hmac> (both providers); the per-project secret is configured on the incoming side of project settings; verification is constant-time; invalid → 401, receipt recorded with signature_valid=false, body NOT processed
  • Project routing: match the payload's repository URL (clone/html URL, normalized like git_repository_url) against projects; no match → receipt stored with project_id NULL, status ignored
  • Idempotency: UNIQUE(source, delivery_id) (GitHub X-GitHub-Delivery, Gitea X-Gitea-Delivery); duplicate delivery → 200, no reprocessing
  • Receipts: every request stored raw (payload text) with event_type, status (pending → processed | failed | ignored), failure_reason
  • v1 automation — PR merged closes issues:
    • On pull_request event with action=closed + merged=true: scan PR title + body for close[sd]? #(\d+) / fix(e[sd])? #(\d+) / resolve[sd]? #(\d+) (case-insensitive)
    • Each #N resolving to an open issue in the routed project → set status done as system user (id 1) with via_webhook=true; emits normal issue.status_changed (so notifications/SSE/search/outgoing webhooks all fire); comment-like audit trail shows the PR URL in the event payload
    • Issues already closed → skipped, noted in receipt
  • Replay: POST /projects/{id}/webhook_receipts/{rid}/replay re-runs processing on the stored payload (owner only)
  • Push events: recorded as receipts, ignored in v1 (future: commit-message automation)

Outgoing webhooks

  • Per-project registrations (owner-managed): url, secret, active, event_types[] (empty = all)
  • Delivery payload (JSON): {event: "issue.status_changed", timestamp, project: {id, title}, actor: {id, username}, resource: {type, id, issue_num?, title?}, changes: {field: {prev, new}}}
  • Signing: X-SoloPM-Signature: sha256=<hex hmac-sha256(secret, raw body)> + X-SoloPM-Event + X-SoloPM-Delivery (delivery id) headers
  • Delivery via webhook_delivery jobs: 10s timeout, any 2xx = success; failure → queue backoff (30s·2^n, 8 attempts) → delivery row failed
  • Delivery log per webhook: status, response code, truncated response body, attempts, timestamps; visible in project settings
  • POST …/webhooks/{whId}/test — sends a {event: "webhook.test"} payload immediately, records a delivery row
  • Never deliver to internal targets: resolve the URL host and reject private/loopback ranges (SSRF guard), both at registration and at delivery time

Export

  • GET /projects/{id}/export?format=json — streaming download solopm-{project}-{date}.json:
    • Full project dump: project, members (user refs by username/email), labels, epics, issues (with sub-issue/blocker/epic relations by project_issue_id), comments, links, wiki pages, attachment metadata (files not embedded; paths listed for manual copy)
    • Versioned envelope {solopm_export: 1, exported_at, data: {...}} — designed to be re-importable later (import itself is out of scope v1, 17-decisions.md)
  • GET /projects/{id}/export?format=csv — issues as CSV: number,title,status,priority,estimate,assignee,epic,labels,parent,start_date,target_date,completed_at,created_at,updated_at (RFC 4180 quoting)
  • Both owner-or-member accessible, streamed (no buffering whole dump in memory), audit-logged as project.exported (03-domain-events.md)