feat(telltale): the ingest Worker (P1) - #64
Conversation
…n fake - Add restClient tests against a stub fetch: label-drop true/false, PR discrimination + state_reason passthrough in findByLabel, and a non-2xx throw (an empty array would read as 'no match' and open a duplicate). - src/github.ts: add the missing Content-Type: application/json header — without it a string body defaults to text/plain per the Fetch spec and GitHub reads the POST as text, not JSON. - test/fakes.ts: FakeGitHub now computes labelsDropped with the same content-based check as the real client (not a count), and gains a selective dropLabel knob alongside the existing all-or-nothing dropLabels, so tests can express a partial drop.
…fuse redirects
tenzy was pointed at adbarc92/tenzy under the primary PAT. That path is only a
transfer redirect - `gh api` follows it silently, which is how the value got in -
and the real repo is OpenBarclay/tenzy. The primary PAT's resource owner is the
adbarc92 user, so it cannot write to an org repo at all; and with Workers'
default redirect:'follow' the 301 would have rewritten the POST into a GET,
making createIssue parse an issue array as an object.
So also: api() now uses redirect:'manual' and throws naming the new location, so
a repo transfer is a loud, diagnosable failure rather than a shape mismatch far
downstream.
unauthorized() compared against `Bearer ${env.OPERATOR_READ_TOKEN}`. With the
secret unset - the first-deploy state, since this Worker has never deployed -
the expected header was the literal "Bearer undefined", a guessable password for
every bug-report body in every private registry repo. A missing secret now
denies everything, and the compare reuses the constant-time equals() from
src/auth.ts rather than a second implementation.
README: create adbarc92/telltale-intake and adbarc92/telltale-probe before
deploying - both 404 today, so GET /v1/issues would carry a permanent errors
entry from its first request and the live grader could never pass. "Adding a
project" now states that `account` must name the PAT whose resource owner
actually owns the repo.
…ary, record duplicate_fingerprint
shouldComment keyed on `ct:${fp}:${bucket()}`, but the fingerprint is a hash of
the scrubbed title alone and KV is one namespace across all eleven projects. Two
apps whose users type "Save button does nothing" share a fingerprint, so the
second project's genuine recurrence comment was silently suppressed for an hour.
The key is now `ct:${project}:${fp}:${bucket()}`, consistent with `rl:proj:`.
export default { fetch } had no try/catch, and fail() awaited recordStat before
building the response. Every counter here is a hot key by construction and KV
allows roughly one write per second per key, so under real traffic the throw
escaped handleEvent and the client got Cloudflare's bare 5xx - no JSON envelope
and no stat, the exact silent failure /v1/stats exists to eliminate. recordStat
now swallows its own rejection (covering every call site) and fetch returns
json(500, { error: 'internal' }) for anything else that escapes.
duplicate_fingerprint was declared and required by the spec but unreachable. It
is now recorded alongside 'accepted' when the label lookup returns more than one
open non-PR candidate - the spec knowingly accepts a concurrent-create race, and
that acceptance is only defensible while the operator can see it happening.
Also asserts Retry-After on the 429 test, a binding constraint a refactor could
otherwise drop silently.
|
Closing, not merging — Telltale has moved to its own repo: https://github.com/adbarc92/telltale. The work here is sound and fully reviewed; it was simply in the wrong repository. "Put it in Command Center" answered a question about where the spec document should live, and was extended to the implementation without ever being put to the operator. Nothing is lost. All 17 commits touching
Nothing needs stripping from this repo. The two docs on this branch that were not about Telltale's implementation — What stays in this repo is the integration only: a |
Telltale now lives at adbarc92/telltale, extracted with git subtree split so all 17 commits kept their TDD and review history. Verified before pushing: 82 passed / 1 skipped, tsc --noEmit exit 0. Also records that the handoff's step 3 was a no-op. telltale/ never existed on main or on this branch — only on PR #64's branch — so closing that PR rather than merging it left nothing to strip, the vitest (telltale) CI job included. #64 and #63 are both closed.
Implements P1 of the Telltale spec — the standalone Cloudflare Worker that turns authenticated bug reports from the portfolio's apps and games into deduplicated GitHub issues.
Spec and plan are in #63. Stacked on #62 (the embargo-guard removal) — merge #62 first; this PR retargets to
mainautomatically when it lands.What this is
Three routes:
POST /v1/events(HMAC-authenticated bug intake), plusGET /v1/issuesandGET /v1/statsbehind an operator token. Crashes deliberately do not flow through this Worker — Sentry's native GitHub integration owns that path with no Telltale code, which is what let the original design lose half its mass.Every decision that can be a pure function is one (
schema,scrub,fingerprint,auth,decide,registry), so the whole policy is unit-tested with no network, no KV and no mocking framework — 82 tests in ~200ms. Two I/O modules sit behind narrow interfaces the tests replace with fakes.index.tsis wiring.Zero runtime dependencies. WebCrypto only, never
node:crypto. Newvitest (telltale)CI job modelled on the existingvitest (cockpit/ui)one.Not in this PR
The senders (P2 — four reference files across nine other repos) and the Command Center dashboard adapter (P3, which consumes
GET /v1/issues) are separate subsystems and get their own plans. P0 is Sentry console configuration.Review process
Eleven tasks, each with an independent scoped review, then a whole-branch review and a final fix wave. That surfaced six defects in the plan's own reference code, all fixed here:
tenzypointed atadbarc92/tenzyOpenBarclay/tenzy. The primary PAT can't write to an org repo, and a followed 301 turns a POST into a GET.not_plannednot aggregated across closed duplicatesContent-Type: application/jsonrestClienthad no tests at allJSON.parseof operator secrets/v1/statsexists to eliminate.401returned before the registry lookup404on a typo'd slug unreachable.The final review also found the comment throttle wasn't project-scoped (KV is one namespace across eleven projects, so two apps sharing a generic title suppressed each other), that an unset
OPERATOR_READ_TOKENmade the expected header the guessable literalBearer undefined, and that there was no top-level error boundary.Before deploying
adbarc92/telltale-intakeandadbarc92/telltale-probeare in the registry but do not exist yet — the README's Deploy section now leads with creating them. Until thenGET /v1/issuescarries a permanenterrorsentry forpawsportand the live grader cannot pass.The live grader (
test/live-grader.test.ts) self-skips without credentials and makes no network call, so CI never runs it. It targets a scratch repo, never a product repo, and asserts by reading GitHub back rather than trusting the Worker's own response.