feat(api): push signed delivery events to Kanbanger #634

Merged
gmackie merged 2 commits from feat/kanbanger-notifications into main 2026-10-05 18:29:58 +00:00
Owner

Summary

ForgeGraph now pushes signed delivery events to Kanbanger (https://tasks.gmac.io), so an issue can show its PR updates, CI builds, evidence and attestations (including sidecar results from Veritas, PlayTrek, Preflight and others), readiness verdicts and deploys.

  • Config: each workspace gets a Kanbanger target, { url, secret, enabled }. It is stored as a workspace_integrations row with provider kanbanger.
    • The secret is encrypted at rest with FG_ENCRYPTION_KEY, like every other integration. Reads only return hasSecret.
    • The URL must be https.
    • No migration.
  • Admin surfaces: changing the target needs owner or admin on the workspace's team. All three surfaces use the same storage.
    • REST: GET, PUT and DELETE on /api/fg/integrations/kanbanger. DELETE disables the target and keeps the URL and secret.
    • tRPC: integration.kanbangerStatus, integration.kanbangerConnect, integration.kanbangerDisable.
    • CLI: forge integration kanbanger connect --url <url> --secret-stdin, forge integration kanbanger status and forge integration kanbanger disable.
  • Delivery: events go through the existing durable notification_outbox, which the minute cron drains.
    • Each event is enqueued after the triggering write commits. Enqueueing never throws.
    • When a workspace has no enabled target, an event costs one query and nothing is queued.
    • Every attempt is re-signed with a fresh timestamp, while x-kanbanger-delivery keeps the same eventId.
    • Retries: 5xx, 408, 429 and network errors back off and retry. Any other 4xx is final. A target that was removed or disabled ends the queued row as cancelled.
    • Dedupe: the eventId comes from a per-event dedupe key. Webhook redeliveries collapse into one event, and so do events reported at two layers (merge route plus Forgejo merge webhook, workflow_run plus CI report).
  • Envelope: version 1, exactly as specified. The shared signature test vector is a unit test, along with a test that serialises the vector envelope byte for byte.

Hooked event sites

  • changeset.opened: onChangesetOpened. It covers all 7 creation paths and skips default-branch and draft rows.
  • changeset.updated: applyBranchPush, the Forgejo push handler and the agent changeset-upsert, each when the head moves. Also the Forgejo PR webhook on opened/reopened/edited, which is when the PR link becomes known.
  • changeset.merged: the agent merge-report, POST /api/fg/prs/[id]/merge, the tRPC changeset.merge, the Forgejo PR webhook (merges done in the Forgejo UI) and the merged-branch reconciler.
  • changeset.closed: the Forgejo PR webhook, a Forgejo branch delete, and POST /api/fg/prs/[id]/close.
  • review.approved / review.changes_requested: the tRPC review.submit and the Forgejo review webhook.
  • build.*:
    • Forgejo workflow_run (started and terminal).
    • /api/fg/ci/report, whose summary comes from the check-events counts, e.g. "2 of 214 tests failed".
    • The agent ci-report (claim and terminal).
    • /api/evidence build evidence.
  • attestation.*:
    • The sidecar POST /api/v1/attestations, with the producer set to the sidecar name.
    • The rule-engine satisfy and fail transitions.
    • Push invalidation, which sets the attestation back to pending.
    • Waive and unwaive.
    • The attestation-timeout cron.
  • readiness.changed: from checkAndFireReadinessWebhook, baselined on the last readiness event sent. Changesets with no work-item link are covered too.
  • deploy.*:
    • The agent deploy report (started, active, failed).
    • /api/fg/deploys/complete (Workers CI deploys).
    • Control Panel deployment callbacks.
    • ForgeGraph deploy dispatch failure.

Verification

  • api unit lane: 1316 tests pass, plus 20 new Kanbanger unit tests.
  • api database lane: 37 files and 264 tests pass, plus 12 new Kanbanger DB tests covering enqueue, dedupe, disabled or unconfigured sends nothing, retry vs terminal vs cancelled, and re-signing on every attempt.
  • api concurrency lane and web database lane are green against a real Postgres.
  • web unit and mobile-config lanes are green. The reconciler test mock was updated for .returning().
  • api and web typecheck are clean, oxlint reports no errors, and go vet and go test ./cmd/fg/... are green.

Not done here

  • No Kanbanger target is configured in any workspace. That is an operator step after deploy.
  • deploy.url is sent only when a Control Panel callback provides one. ForgeGraph does not store a per-stage URL.

🤖 Generated with Claude Code

## Summary ForgeGraph now pushes signed delivery events to Kanbanger (https://tasks.gmac.io), so an issue can show its PR updates, CI builds, evidence and attestations (including sidecar results from Veritas, PlayTrek, Preflight and others), readiness verdicts and deploys. - **Config:** each workspace gets a Kanbanger target, `{ url, secret, enabled }`. It is stored as a `workspace_integrations` row with provider `kanbanger`. - The secret is encrypted at rest with FG_ENCRYPTION_KEY, like every other integration. Reads only return `hasSecret`. - The URL must be https. - **No migration.** - **Admin surfaces:** changing the target needs owner or admin on the workspace's team. All three surfaces use the same storage. - REST: `GET`, `PUT` and `DELETE` on `/api/fg/integrations/kanbanger`. DELETE disables the target and keeps the URL and secret. - tRPC: `integration.kanbangerStatus`, `integration.kanbangerConnect`, `integration.kanbangerDisable`. - CLI: `forge integration kanbanger connect --url <url> --secret-stdin`, `forge integration kanbanger status` and `forge integration kanbanger disable`. - **Delivery:** events go through the existing durable `notification_outbox`, which the minute cron drains. - Each event is enqueued after the triggering write commits. Enqueueing never throws. - When a workspace has no enabled target, an event costs one query and nothing is queued. - Every attempt is re-signed with a fresh timestamp, while `x-kanbanger-delivery` keeps the same `eventId`. - Retries: 5xx, 408, 429 and network errors back off and retry. Any other 4xx is final. A target that was removed or disabled ends the queued row as `cancelled`. - **Dedupe:** the `eventId` comes from a per-event dedupe key. Webhook redeliveries collapse into one event, and so do events reported at two layers (merge route plus Forgejo merge webhook, `workflow_run` plus CI report). - **Envelope:** version 1, exactly as specified. The shared signature test vector is a unit test, along with a test that serialises the vector envelope byte for byte. ## Hooked event sites - **changeset.opened:** `onChangesetOpened`. It covers all 7 creation paths and skips default-branch and draft rows. - **changeset.updated:** `applyBranchPush`, the Forgejo push handler and the agent changeset-upsert, each when the head moves. Also the Forgejo PR webhook on opened/reopened/edited, which is when the PR link becomes known. - **changeset.merged:** the agent merge-report, `POST /api/fg/prs/[id]/merge`, the tRPC `changeset.merge`, the Forgejo PR webhook (merges done in the Forgejo UI) and the merged-branch reconciler. - **changeset.closed:** the Forgejo PR webhook, a Forgejo branch delete, and `POST /api/fg/prs/[id]/close`. - **review.approved / review.changes_requested:** the tRPC `review.submit` and the Forgejo review webhook. - **build.\*:** - Forgejo `workflow_run` (started and terminal). - `/api/fg/ci/report`, whose summary comes from the check-events counts, e.g. "2 of 214 tests failed". - The agent ci-report (claim and terminal). - `/api/evidence` build evidence. - **attestation.\*:** - The sidecar `POST /api/v1/attestations`, with the producer set to the sidecar name. - The rule-engine satisfy and fail transitions. - Push invalidation, which sets the attestation back to pending. - Waive and unwaive. - The attestation-timeout cron. - **readiness.changed:** from `checkAndFireReadinessWebhook`, baselined on the last readiness event sent. Changesets with no work-item link are covered too. - **deploy.\*:** - The agent deploy report (started, active, failed). - `/api/fg/deploys/complete` (Workers CI deploys). - Control Panel deployment callbacks. - ForgeGraph deploy dispatch failure. ## Verification - api unit lane: 1316 tests pass, plus 20 new Kanbanger unit tests. - api database lane: 37 files and 264 tests pass, plus 12 new Kanbanger DB tests covering enqueue, dedupe, disabled or unconfigured sends nothing, retry vs terminal vs cancelled, and re-signing on every attempt. - api concurrency lane and web database lane are green against a real Postgres. - web unit and mobile-config lanes are green. The reconciler test mock was updated for `.returning()`. - api and web typecheck are clean, oxlint reports no errors, and `go vet` and `go test ./cmd/fg/...` are green. ## Not done here - No Kanbanger target is configured in any workspace. That is an operator step after deploy. - `deploy.url` is sent only when a Control Panel callback provides one. ForgeGraph does not store a per-stage URL. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): push signed delivery events to Kanbanger
Some checks failed
CI / gitleaks (pull_request) Successful in 8s
CI / storybook (pull_request) Successful in 1m58s
forgegraph/ci CI failed
CI / ci (pull_request) Failing after 2m2s
CI / web-build (pull_request) Successful in 3m38s
cd9975a5af
Kanbanger (tasks.gmac.io) shows an issue's PR, CI, evidence, readiness and
deploys. ForgeGraph now pushes those as signed, versioned events instead of
Kanbanger polling for them.

Configuration is a per-workspace `workspace_integrations` row with provider
"kanbanger": the HMAC secret is the encrypted credential (FG_ENCRYPTION_KEY,
same as every integration) and { url, enabled } is its metadata, so there is
no schema change. Reads expose hasSecret, never the secret. Managed through
GET/PUT/DELETE /api/fg/integrations/kanbanger, the integration.kanbanger*
tRPC procedures, and `forge integration kanbanger connect --url
--secret-stdin | status | disable`. Writes need team owner/admin.

Emission only enqueues into the existing notification_outbox after the
triggering write commits; it never throws and costs one query when the
workspace has no enabled target (zero behaviour change). The minute cron
drain signs each attempt with a fresh timestamp
(sha256=HMAC(secret, "v1:" + ts + ":" + body)) while x-kanbanger-delivery
stays the stable eventId. 5xx/408/429 and network errors retry with the
outbox backoff; other 4xx, or a disabled/removed target, end the row.
eventIds derive from a per-event dedupe key, so webhook redeliveries and
layered sites (merge route + Forgejo merge webhook, workflow_run + ci
report) collapse onto one event.

Hooked: changeset opened/updated/merged/closed (lifecycle, branch push,
Forgejo push/PR webhooks, agent upsert/merge-report, PR merge/close routes,
tRPC merge, merged-branch reconciler); reviews (tRPC + Forgejo webhook);
builds (Forgejo workflow_run, CI report, agent ci-report, evidence API);
attestations (sidecar POST /api/v1/attestations with producer name, rule
engine, push invalidation, waivers, timeout cron); readiness.changed (from
the readiness check, baselined on the last event sent so unlinked
changesets work too); deploys (agent deploy report, CI deploy completion,
Control Panel callbacks, dispatch failure).

Evidence: api unit 1316 + new 20 (incl. the shared signature vector), api
database lane 37 files / 264 tests plus 12 new Kanbanger DB tests (enqueue,
dedupe, disabled => nothing, retry/terminal/cancel, re-signing), api
concurrency and web database lanes green on a real Postgres, web unit
lanes green, api + web typecheck clean, oxlint clean (warnings only), go
vet + go test ./cmd/fg/... green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(api): narrow the readiness baseline payload with its guard
All checks were successful
CI / gitleaks (pull_request) Successful in 9s
CI / storybook (pull_request) Successful in 1m35s
CI / web-build (pull_request) Successful in 3m36s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 12m41s
00bf0f29db
The mobile typecheck (stricter lib settings) rejects casting the outbox's
Record<string, unknown> payload straight to KanbangerOutboxPayload. Use the
existing isKanbangerOutboxPayload guard instead of an assertion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gmackie/ForgeGraph!634
No description provided.