Skip to content

Architecture

Overview

GitHub (webhooks / API)
Cloudflare Worker (Hono)
  ├── POST /api/webhook     ← receives GitHub events
  ├── GET  /api/badge/…     ← serves dynamic SVG
  ├── PATCH /api/state/…    ← updates checklist state
  └── GET  /auth/*          ← GitHub OAuth flow

Cloudflare Pages
  └── GET /checklist/…      ← checklist editor UI

Key design decisions

No external database

All checklist state is stored inside a hidden HTML comment (<!-- CHECKLIST_STATE: base64(json) -->) embedded in the bot's managed comment on the issue. GitHub is both the data store and the source of truth.

Why: Eliminates operational overhead (no DB to provision, back up, or scale), keeps all data within GitHub's own data residency guarantees, and makes state auditable by any repo collaborator with comment-edit access.

Trade-off: State updates require a GitHub API write, and reads require fetching the comment body. Both are subject to GitHub rate limits, mitigated by the 60-second edge cache on the badge endpoint.

GitHub App (not plain OAuth)

A GitHub App provides webhook delivery and bot identity. Plain OAuth would require polling, wouldn't have a stable bot identity for the managed comment, and couldn't act on behalf of the installation.

Cloudflare Workers + Hono

The webhook handler and API routes run on Cloudflare Workers. Hono provides a minimal, type-safe routing layer that maps cleanly to the Workers fetch handler.

Why over Next.js + Vercel: Workers deploy globally with zero cold starts, and the edge cache on the badge endpoint is native. Hono adds less than 15 KB to the bundle.

Managed comment strategy

The app posts exactly one comment per issue. That comment is owned by the bot and updated in-place. This avoids cluttering the issue timeline with many bot comments and keeps the state co-located with its rendered output.

The comment body contains:

  1. A rendered human-readable checklist (Markdown)
  2. A hidden HTML comment with the serialized state blob
  3. A link to the checklist editor UI

Template cascade

Templates cascade from the most specific (repo) to least specific (org-wide public), allowing teams to define org-wide defaults while letting individual repos override them. The private org repo (.github-private) lets you have non-public org-wide templates.

Merge on type change

When an issue type changes, progress on matching items is preserved. Items are matched by label text, so renaming a label in the template breaks the match and resets that item to pending. This is intentional — a label change signals a different check.

Data flow: issue opened

issues.opened webhook
1. Read issue.type.name from payload
2. Slugify → e.g. "Bug Report" → "bug-report"
3. Try repo/.github/CHECKLIST/bug-report.md   (404 → continue)
4. Try org/.github-private/.github/CHECKLIST/bug-report.md   (404 → continue)
5. Try org/.github/.github/CHECKLIST/bug-report.md   (404 → use built-in default)
6. Parse template → initial state (all items "pending")
7. Render comment body (Markdown checklist + hidden state blob)
8. POST comment to GitHub API

Data flow: state update

User clicks item in editor UI
PATCH /api/state/:owner/:repo/:issue
        ├── Verify GitHub OAuth session
        ├── Verify collaborator access (GitHub API)
        ├── If status=exception: verify approver rules (GitHub API)
        ├── Fetch managed comment (GitHub API)
        ├── Decode state blob
        ├── Apply patch
        ├── Re-encode state blob
        └── PATCH comment body (GitHub API)