Building an AI-drafted changelog tool with a GitHub App and Claude, solo
Building an AI-drafted changelog tool with a GitHub App and Claude, solo
独立开发:利用 GitHub App 和 Claude 构建 AI 自动生成更新日志的工具
I’ve been building a hosted changelog tool called Chngd, mostly solo, using Claude Code as a daily collaborator rather than an autocomplete tool. Here’s what the actual build looked like, technically, and a mistake that turned into the most useful lesson so far. 我一直在独立开发一款名为 Chngd 的托管式更新日志工具。在开发过程中,我将 Claude Code 作为日常协作伙伴,而非仅仅是一个自动补全工具。以下是该项目的技术构建过程,以及一个让我受益匪浅的错误教训。
The core loop
核心流程
Most changelog tools ask you to write the changelog. Chngd tries to remove that step: connect a GitHub repo, and it drafts a customer-facing changelog entry from your recent commits and PRs. You review, edit, and publish. Nothing goes out automatically. 大多数更新日志工具都需要你手动撰写内容。Chngd 旨在省去这一步:只需连接 GitHub 仓库,它就会根据你最近的提交(commits)和合并请求(PRs)自动起草一份面向客户的更新日志。你负责审核、编辑并发布,没有任何内容会自动发布。
The pipeline is simple on paper: 从理论上讲,其流水线非常简单:
- Pull commits since the last published entry
- 获取自上次发布以来的所有提交
- Filter out anything that’s clearly not customer-facing (chore:, ci:, test:, docs: prefixes, [skip changelog])
- 过滤掉明显不面向客户的内容(如 chore:、ci:、test:、docs: 前缀,或包含 [skip changelog] 的提交)
- Send what’s left to Claude with a system prompt telling it the audience is non-technical end users, not developers
- 将剩余内容发送给 Claude,并附带系统提示词,明确受众是非技术背景的终端用户,而非开发者
- Get back structured JSON: a title and a markdown body
- 获取结构化的 JSON 数据:包含标题和 Markdown 正文
- Show it in a dashboard for editing before publish
- 在仪表盘中展示,供发布前编辑
The filtering step matters more than I expected. Sending raw commit logs to a model produces a changelog that reads like a commit log with better grammar. Filtering internal-only commits first, before the model ever sees them, is what actually makes the output read like something a customer would want. 过滤步骤的重要性超出了我的预期。如果直接将原始提交日志发送给模型,生成的更新日志读起来就像是语法优化版的提交记录。只有在模型处理前先过滤掉内部提交,输出的内容才真正符合客户的阅读需求。
GitHub App vs OAuth+PAT
GitHub App 与 OAuth+PAT 的抉择
The lazier option was OAuth plus a personal access token. It’s faster to build, maybe half a day faster. I went with a GitHub App instead, and the reason came down to one question: what does the paid tier actually need? 更省事的方案是使用 OAuth 加上个人访问令牌(PAT)。这种方式开发更快,大约能节省半天时间。但我最终选择了 GitHub App,原因归结为一个问题:付费层级到底需要什么功能?
The free tier is a manual “sync now” button. The paid tier auto-syncs on push, via webhook, and auto-drafts (never auto-publishes, that stays a human action). That’s fundamentally a GitHub App feature, not an OAuth+PAT one, because per-repo webhooks and installation-scoped access are how GitHub Apps work. 免费层级提供手动“立即同步”按钮。付费层级则通过 Webhook 实现推送时自动同步,并自动起草(绝不自动发布,发布仍需人工操作)。这本质上是 GitHub App 的特性,而非 OAuth+PAT 的功能,因为基于仓库的 Webhook 和安装范围内的访问权限正是 GitHub App 的工作方式。
Building it on OAuth+PAT first would have meant asking every existing user to redo their connection later. Paying the extra setup cost upfront avoided a migration I’d have hated doing after launch. 如果起初选择 OAuth+PAT,意味着以后必须要求所有现有用户重新进行连接配置。预先支付额外的开发成本,避免了我在产品发布后不得不进行的繁琐迁移。
The mistake: Supabase RLS
错误教训:Supabase RLS
This is the part worth writing about honestly instead of skipping. I’m using Supabase for Postgres plus Auth, but all the actual data access happens server-side through Drizzle, not through Supabase’s client libraries. Because of that, I treated Row-Level Security as something that applied to a pattern I wasn’t using, and didn’t think hard about it. 这一部分值得诚实地记录下来,而不是避而不谈。我使用 Supabase 提供 Postgres 数据库和身份验证,但所有实际的数据访问都是通过 Drizzle 在服务端完成的,而不是通过 Supabase 的客户端库。正因如此,我误以为行级安全性(RLS)只适用于我没用到的那种模式,所以没太在意。
That was wrong. Supabase’s REST API (PostgREST) sits in front of every table regardless of how your own server code talks to the database. The public anon key is baked into client-side JS by design, it’s meant to be public. Without RLS enabled, that anon key can read and write any table with no policy in place, completely independent of whatever access patterns your app code uses. 我错了。无论你的服务端代码如何与数据库交互,Supabase 的 REST API (PostgREST) 都位于每个表的前端。公共匿名密钥(anon key)在设计上就嵌入在客户端 JS 中,它是公开的。如果未启用 RLS,该匿名密钥可以在没有任何策略限制的情况下读取和写入任何表,这与你的应用代码所使用的访问模式完全无关。
Every table was exposed like this for weeks before I caught it, via a monitoring alert I’d nearly missed. I audited for tampering (none found), enabled RLS on all six tables, verified with both a direct Postgres query and a live curl test against the REST endpoint with the anon key, and set up Slack alerting so a future alert doesn’t depend on me checking email. 在通过一个差点被我忽略的监控警报发现问题之前,我的所有表就这样暴露了数周。我进行了篡改审计(未发现异常),为所有六个表启用了 RLS,通过直接的 Postgres 查询和使用匿名密钥对 REST 端点进行实时 curl 测试进行了验证,并设置了 Slack 警报,这样以后的警报就不再依赖于我是否查看邮件了。
The lesson: RLS is off by default in Supabase, and “I don’t use the client library for this” is not the same as “this table is protected.” If you’re using Supabase as Postgres+Auth with a server-side ORM, check this today. It costs one migration and a few minutes. 教训是:Supabase 默认关闭 RLS,“我没用客户端库”并不等于“表是受保护的”。如果你正在将 Supabase 作为 Postgres+Auth 使用,并配合服务端 ORM,请务必今天就检查一下。这只需要一次迁移和几分钟时间。
Where it stands
项目现状
Live at chngd.dev, test-mode-to-live Stripe billing, GitHub App install flow, embeddable widget, email notifications on publish. Built and shipped solo. Happy to answer questions about any of the above, especially the Claude integration or the GitHub App setup if anyone’s weighing that same decision. 项目已上线至 chngd.dev,支持 Stripe 测试模式到正式模式的计费、GitHub App 安装流程、可嵌入式小部件以及发布时的电子邮件通知。全程由我一人开发并发布。如果有人正在考虑类似的技术决策,我很乐意回答关于上述任何内容的问题,特别是 Claude 集成或 GitHub App 的设置。