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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Archive Report: GitHub Alerts

**Date**: 2026-09-26
**Change**: github-alerts
**Artifact Store**: hybrid (OpenSpec + Engram)
**Status**: ARCHIVED

## Change Summary

GitHub org webhook alerts delivered to linked Telegram forum topics. It adds an HMAC-verified `POST /github/webhook` route, org claims and repo-topic links in D1, issue and pull request alert routing, and the admin commands `/linkrepo`, `/unlinkrepo` and `/repos`. It shipped in five chained PRs (#12 to #16) after the planning PR #11.

## Artifacts Persisted

### OpenSpec Filesystem (authoritative)
- `openspec/specs/github-alerts/spec.md`: new capability, taken from the delta spec
- `openspec/specs/github-webhook/spec.md`: new capability, taken from the delta spec
- `openspec/specs/repo-topic-links/spec.md`: new capability, taken from the delta spec
- `openspec/changes/archive/2026-09-26-github-alerts/`: the complete change archive, with every artifact kept

### Engram Memory (mirror, traceability)
- `sdd/github-alerts/verify-report`
- `sdd/github-alerts/archive-report`

## Verification

- Verdict: PASS (see `verify-report.md`, including the post-verify resolution)
- Tests: 334/334 passing; typecheck clean
- Tasks: 27/27 checked, including operator rollout tasks 6.1 to 6.3
- Rollout: org webhook `686170412` is active; the signed ping returned 200
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ Chain strategy: stacked-to-main

## Phase 6: Operator Rollout

- [ ] 6.1 (after PR3 merges) `npx wrangler secret put GITHUB_WEBHOOK_SECRET`.
- [ ] 6.2 (after PR4 merges) Configure org webhook: content type `application/json`, same secret, Pull requests + Issues events; verify ping returns 200.
- [x] 6.1 (after PR3 merges) `npx wrangler secret put GITHUB_WEBHOOK_SECRET`.
- [x] 6.2 (after PR4 merges) Configure org webhook: content type `application/json`, same secret, Pull requests + Issues events; verify ping returns 200.
- [x] 6.3 (after PR1 merges, before PR5's `/linkrepo` is used) Claim the org via `wrangler d1 execute` insert into `github_org_claims`.
95 changes: 95 additions & 0 deletions openspec/changes/archive/2026-09-26-github-alerts/verify-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Verification Report: github-alerts

**Change**: github-alerts
**Mode**: Full artifacts (proposal + specs + design + tasks + apply-progress), Strict TDD active
**Date**: 2026-09-26
**Branch state**: `main`, all 5 implementation PRs (#12-#16) merged, working tree clean (only untracked `.codegraph/`)

## Completeness Table

| Phase | Tasks | Status |
|---|---|---|
| Phase 1 — Domain Foundation (PR1) | 1.1-1.9 | 9/9 [x] |
| Phase 2 — D1 Adapters (PR2) | 2.1-2.2 | 2/2 [x] |
| Phase 3 — Signature/Route Skeleton (PR3) | 3.1-3.4 | 4/4 [x] |
| Phase 4 — Delivery Wiring (PR4) | 4.1-4.6 | 6/6 [x] |
| Phase 5 — Link Commands (PR5) | 5.1-5.3 | 3/3 [x] |
| Phase 6 — Operator Rollout | 6.1-6.3 | 1/3 [x] (6.3 done; 6.1/6.2 pending, operator-only, outside code) |

24/26 tasks checked. The 2 unchecked tasks (6.1 `wrangler secret put GITHUB_WEBHOOK_SECRET`, 6.2 configure the GitHub org webhook) are explicitly scoped in tasks.md/apply-progress.md as operator/infrastructure steps outside `sdd-apply`'s scope, not code or test work. They are now unblocked since PR3 and PR4 are merged into `main`.

## Build / Test Evidence (executed this session)

| Command | Result |
|---|---|
| `npm test` (`vitest run`) | **334/334 tests passed**, 38 test files, 0 failed. Matches the count claimed in apply-progress.md's PR5 status. |
| `npm run typecheck` (`tsc --noEmit`) | **Clean, no errors.** |

No coverage command is configured in `package.json`; none was run (none claimed in apply-progress either).

## Spec Compliance Matrix

### `specs/github-alerts/spec.md`

| Requirement | Scenario | Covering test(s) | Status |
|---|---|---|---|
| Route Alert to the Linked Topic Only | Linked repo produces an alert | `test/http/github-webhook-delivery-e2e.test.ts` (delivered case) | PASS |
| Route Alert to the Linked Topic Only | Unlinked repo produces no alert | same file, unlinked-but-claimed-org / unclaimed-org silent cases | PASS |
| Allowlisted Fields Only, No Payload Storage or Logging | Alert contains only allowed fields | `test/adapters/github/event-mapper.test.ts` (commit-email fixture never reaches mapped event) | PASS |
| Allowlisted Fields Only... | Processing error does not log the payload | `test/http/github-webhook.test.ts` / `github-webhook-delivery-e2e.test.ts` no-payload-in-logs assertions | PASS |
| Message Truncated to Telegram's Limit | Long title is truncated | `test/domain/github.test.ts` (`formatGithubAlert` truncation, 4096 cap in `src/domain/github.ts:42,53`) | PASS |
| Delivery Failure Is Logged and Acknowledged | sendMessage fails, topic deleted | `test/http/github-webhook-delivery-e2e.test.ts` (send-failure → 2xx, `reason` logged, exactly-one-call/no-retry assertion) | PASS |

### `specs/github-webhook/spec.md`

| Requirement | Scenario | Covering test(s) | Status |
|---|---|---|---|
| HMAC Signature Verification Over Raw Body | Missing/wrong/right-length-wrong/valid signature | `test/adapters/github/signature.test.ts` (7 tests), `test/http/github-webhook.test.ts` signature gate | PASS |
| Ping Event Acknowledged | GitHub sends a ping | `test/http/github-webhook.test.ts` | PASS |
| Unsupported Event or Action Ignored | Unsupported event type / action | `event-mapper.test.ts` null-return cases, `github-webhook.test.ts` | PASS |
| Infrastructure Failures Return 500 | D1 unavailable during routing | `test/http/github-webhook-delivery-e2e.test.ts` (broken `env.DB` → 500, error-name-only log) | PASS |

### `specs/repo-topic-links/spec.md`

| Requirement | Scenario | Covering test(s) | Status |
|---|---|---|---|
| Org Claim Required for Linking | Claimed org can be linked / unclaimed rejected | `test/domain/link-repo-to-topic.test.ts`, `test/adapters/telegram/commands.test.ts` | PASS |
| Admin-Only Link/Unlink Inside a Topic | Admin ok / non-admin refused / outside-topic refused | `commands.test.ts` (`/linkrepo`/`/unlinkrepo` describe blocks) | PASS |
| One Topic Per Repo, Re-Link Moves It | First link / re-link moves + reply names both topics | `link-repo-to-topic.test.ts` (`previousThreadId`), `commands.test.ts` exact-string re-link reply | PASS |
| Any Member Lists the Team's Claimed-Org Links | Non-admin lists / non-member refused / excludes unclaimed | `list-repo-links.test.ts`, `commands.test.ts` `/repos` describe block, including the RES-001 4096-cap fix | PASS |

**All 15 spec scenarios across 3 spec files have a passing covering test at runtime. No UNTESTED or FAILING scenarios found.**

## Correctness Spot-Checks (source read this session)

- `src/domain/github.ts:11,42-53` — `RepoFullName` branding/lowercasing and `formatGithubAlert` truncation match spec verbatim.
- `src/index.ts:95-167` — `/github/webhook` route: raw-body read guarded (try/catch → 500 on transport failure, per PR3 Correction 2), signature verified before `JSON.parse`, `mapGithubEvent` → `routeGithubEvent` → status mapping wired as documented.
- Test suite file/dir counts (`42` test files under `test/`, `3372` total lines under `src/`) are consistent with the incremental file lists in apply-progress.md across PR1-PR5.

## Design Coherence

All deviations from `design.md` are explicitly disclosed in `apply-progress.md` (e.g., "must be inside a topic" / re-link reply semantics moved to the command/adapter layer instead of the domain use case; PR3's D1-500 scenario deferred to PR4; non-ping events under the PR3 skeleton returning a blanket 200 until the PR4 mapper existed). Each deviation is justified against the design's own contract and none breaks a spec requirement. No unresolved design deviations found.

## Issues

### CRITICAL
None.

### WARNING
1. Tasks 6.1 (`wrangler secret put GITHUB_WEBHOOK_SECRET`) and 6.2 (configure GitHub org webhook: content type, secret, event types, verify ping 200) in `openspec/changes/github-alerts/tasks.md` remain unchecked. These are manual operator/infrastructure actions outside the code repository, not blocked by any remaining code work now that PR3/PR4 are merged into `main`. Recommend the maintainer execute and check these off before considering the feature live in production, though they do not block `sdd-archive` of the code change itself.

### SUGGESTION
None beyond what apply-progress.md's own review-ledger corrections already addressed (all CRITICAL/WARNING findings from PR1-PR5's internal review cycles were fixed in-flight, per the frozen-ledger tables in apply-progress.md, and are not re-litigated here).

## Final Verdict

**PASS WITH WARNINGS** — all 24 code tasks complete, all 15 spec scenarios covered by passing runtime tests (334/334), typecheck clean, no design-breaking deviations. The only open item is the 2 unchecked operator-rollout tasks (6.1/6.2), which are infrastructure actions outside code scope.

### Post-verify resolution (2026-09-26)

The WARNING above is resolved. Tasks 6.1 and 6.2 are now checked in `tasks.md`.

- 6.1: `GITHUB_WEBHOOK_SECRET` is set on the `hack-bot` worker (confirmed with `wrangler secret list`). Before the secret was set, the route answered 500, as the design expects.
- 6.2: Org webhook `686170412` on `Zer0-Knowledge-Hack` (content type JSON, events `issues` and `pull_request`, URL `https://hack-bot.juliocesarsevericheorellana.workers.dev/github/webhook`). The signed ping delivery returned **200 OK**, which proves the GitHub and Cloudflare secrets match (a mismatch returns 401).

Final verdict: **PASS**.
64 changes: 64 additions & 0 deletions openspec/specs/github-alerts/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# GitHub Alerts Specification

## Purpose

Routes supported GitHub events for linked repos to their linked forum topic as a minimal-field alert message, without storing or logging raw webhook payloads.

## Requirements

### Requirement: Route Alert to the Linked Topic Only

The system MUST deliver an alert only to the forum topic linked to the event's repo for the claiming team. Events for repos with no link, or whose org has no claim, MUST produce no alert.

#### Scenario: Linked repo produces an alert

- GIVEN `owner/repo` is linked to a topic for the claiming team
- WHEN a supported event/action fires for `owner/repo`
- THEN the system sends one alert message to that topic

#### Scenario: Unlinked repo produces no alert

- GIVEN `owner/repo` has no link for any team, or its org is unclaimed
- WHEN a supported event/action fires for `owner/repo`
- THEN the system MUST NOT send any message
- AND MUST NOT fall back to any other channel

### Requirement: Allowlisted Fields Only, No Payload Storage or Logging

The system MUST build the alert message using only repo full name, action, actor login, number, title, and URL, and MUST NOT persist or log the raw webhook payload.

#### Scenario: Alert contains only allowed fields

- GIVEN a supported `pull_request` event for a linked repo
- WHEN the alert message is built
- THEN it contains only repo, action, actor login, number, title, and URL
- AND contains no other payload fields (e.g. no commit emails, no diff content)

#### Scenario: Processing error does not log the payload

- GIVEN an error occurs while handling a webhook event
- WHEN the system logs the error
- THEN the log entry MUST NOT contain the raw request body or any payload field values

### Requirement: Message Truncated to Telegram's Limit

The system MUST truncate the built alert message so it never exceeds Telegram's 4096-character limit before sending.

#### Scenario: Long title is truncated

- GIVEN an issue or PR title long enough that the built message would exceed 4096 characters
- WHEN the alert message is built
- THEN the system truncates it so the final message is at most 4096 characters
- AND the message remains a valid, sendable text

### Requirement: Delivery Failure Is Logged and Acknowledged

The system MUST log a delivery failure (e.g. the linked topic was deleted) by reason only, without the payload, and MUST still return a 2xx response for the webhook request.

#### Scenario: sendMessage fails because the topic was deleted

- GIVEN a repo is linked to a topic that has since been deleted
- WHEN a supported event fires and delivery to that topic fails
- THEN the system logs the failure reason only
- AND the webhook HTTP response is still 2xx
- AND no retry is attempted within the same request
77 changes: 77 additions & 0 deletions openspec/specs/github-webhook/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# GitHub Webhook Specification

## Purpose

Verifies that inbound GitHub webhook deliveries are authentic and filters them to the supported event/action set before any org, team, or repo routing happens.

## Requirements

### Requirement: HMAC Signature Verification Over Raw Body

The system MUST verify the `X-Hub-Signature-256` header via HMAC-SHA256 over the raw request body bytes, computed with the global Worker secret `GITHUB_WEBHOOK_SECRET`, using a constant-time comparison, and MUST perform this check before parsing the body as JSON.

#### Scenario: Missing signature header

- GIVEN a webhook POST arrives with no `X-Hub-Signature-256` header
- WHEN the request is received
- THEN the system MUST reject it before parsing the body
- AND MUST NOT process any event

#### Scenario: Wrong signature

- GIVEN a webhook POST arrives with an `X-Hub-Signature-256` header computed from a different secret
- WHEN the signature is verified against the raw body
- THEN the system MUST reject the request before parsing the body

#### Scenario: Right-length but wrong signature

- GIVEN a webhook POST arrives with a signature of the correct hex length but incorrect content
- WHEN the signature is verified using constant-time comparison
- THEN the system MUST reject the request
- AND the rejection MUST NOT leak timing information distinguishing this case from a random wrong-length signature

#### Scenario: Valid signature

- GIVEN a webhook POST arrives with a signature matching the raw body under `GITHUB_WEBHOOK_SECRET`
- WHEN the signature is verified
- THEN the system parses the body and continues processing

### Requirement: Ping Event Acknowledged

The system MUST respond with a 2xx status to a `ping` event, once signature-verified, without further processing.

#### Scenario: GitHub sends a ping

- GIVEN a signature-verified webhook delivery with event type `ping`
- WHEN the request is processed
- THEN the system MUST return a 2xx response
- AND MUST NOT attempt org, repo, or alert routing

### Requirement: Unsupported Event or Action Ignored

The system MUST acknowledge with a 2xx response and drop, without producing an alert, any event/action combination outside the supported set (`pull_request` opened, closed, review_requested; `issues` opened, closed).

#### Scenario: Unsupported event type

- GIVEN a signature-verified webhook delivery with an event type outside the supported set (e.g. `push`)
- WHEN the request is processed
- THEN the system MUST return a 2xx response
- AND MUST NOT produce an alert

#### Scenario: Supported event, unsupported action

- GIVEN a signature-verified `pull_request` event with an action outside the supported set (e.g. `labeled`)
- WHEN the request is processed
- THEN the system MUST return a 2xx response
- AND MUST NOT produce an alert

### Requirement: Infrastructure Failures Return 500

After a valid signature, the system MUST return a 500 response when an unexpected infrastructure failure (e.g. a D1 error) prevents routing, and MUST log it by error name only. This leaves the delivery marked as failed in GitHub so an operator can redeliver it manually. A Telegram delivery failure (e.g. the linked topic was deleted) is NOT an infrastructure failure: it follows the delivery-failure requirement and still returns 2xx, because redelivering cannot fix it.

#### Scenario: D1 is unavailable during routing

- GIVEN a signature-verified supported event
- WHEN reading the org claim or repo link fails with an unexpected error
- THEN the system MUST return a 500 response
- AND MUST log the failure by error name, without the payload
89 changes: 89 additions & 0 deletions openspec/specs/repo-topic-links/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Repo-Topic Links Specification

## Purpose

Manages the org claim and the per-team mapping of GitHub repos to forum topics, enforcing that only claimed orgs can be linked and that link mutations are admin-only.

## Requirements

### Requirement: Org Claim Required for Linking

The system MUST allow linking a repo to a topic only if the repo's GitHub org has a `github_org_claims` row binding it to the requesting team. The claim row MUST be created by a one-time operator D1 step; no in-product command creates it in this change.

#### Scenario: Claimed org repo can be linked

- GIVEN the team's org has a `github_org_claims` row for `owner`
- WHEN a team admin runs `/linkrepo owner/repo` inside a topic
- THEN the system creates the link between `owner/repo` and that topic

#### Scenario: Unclaimed org repo is rejected

- GIVEN no `github_org_claims` row exists for `owner`
- WHEN a team admin runs `/linkrepo owner/repo` inside a topic
- THEN the system MUST refuse to create the link
- AND MUST NOT store any row for that repo

### Requirement: Admin-Only Link/Unlink Inside a Topic

The system MUST allow `/linkrepo` and `/unlinkrepo` only when run by a team admin inside a forum topic, and MUST refuse otherwise.

#### Scenario: Admin runs /linkrepo inside a topic

- GIVEN the caller is a team admin
- WHEN they run `/linkrepo owner/repo` inside a forum topic
- THEN the system processes the link request

#### Scenario: Non-admin attempts to link or unlink

- GIVEN the caller is not a team admin
- WHEN they run `/linkrepo` or `/unlinkrepo` inside a topic
- THEN the system MUST refuse
- AND MUST NOT change any stored link

#### Scenario: Admin runs the commands outside a topic

- GIVEN the caller is a team admin
- WHEN they run `/linkrepo` or `/unlinkrepo` in the group's general chat (not inside a topic)
- THEN the system MUST refuse
- AND MUST instruct the admin to run it inside the intended topic

### Requirement: One Topic Per Repo, Re-Link Moves It

The system MUST allow a repo to be linked to at most one topic per team. Linking an already-linked repo to a different topic MUST move the mapping and MUST tell the admin the previous topic is no longer receiving alerts for that repo.

#### Scenario: First link

- GIVEN `owner/repo` has no existing link for the team
- WHEN an admin runs `/linkrepo owner/repo` inside topic A
- THEN the system creates a link from `owner/repo` to topic A

#### Scenario: Re-link moves the repo

- GIVEN `owner/repo` is linked to topic A for the team
- WHEN an admin runs `/linkrepo owner/repo` inside topic B
- THEN the system updates the link to point to topic B
- AND the reply states the repo moved from topic A to topic B

### Requirement: Any Member Lists the Team's Claimed-Org Links

The system MUST allow any registered member of the team to run `/repos` anywhere in the team's group (general chat or any topic), and MUST refuse non-members. Listing is read-only. `/repos` MUST list only links for repos belonging to orgs the team has claimed.

#### Scenario: Non-admin member lists links

- GIVEN the caller is a registered team member who is not an admin
- WHEN they run `/repos` in the group's general chat
- THEN the reply lists the team's links
- AND no stored link changes

#### Scenario: Non-member runs /repos

- GIVEN the caller is not a registered member of the team
- WHEN they run `/repos` in the group
- THEN the system MUST refuse

#### Scenario: List reflects current links

- GIVEN the team has links for `owner/repo-a` and `owner/repo-b`
- WHEN a team member runs `/repos`
- THEN the reply lists both repos and their linked topics
- AND excludes any link that no longer has a matching org claim
Loading