Skip to content

Add cluster - #1

Merged
IMax153 merged 82 commits into
mainfrom
cluster
Sep 8, 2026
Merged

IMax153 merged 82 commits into
mainfrom
cluster

Conversation

@IMax153

@IMax153 IMax153 commented Sep 8, 2026

Copy link
Copy Markdown
Member

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

Related

  • Related Issue #
  • Closes #

IMax153 and others added 30 commits September 1, 2026 14:21
Read the GitHub webhook body as a bounded stream and enforce the size
limit on actual bytes, independent of Content-Length. Stream read
failures now map to 400 instead of 413.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bodies over 64 KiB are written to R2 before the Queue message carries
their reference. Ingress returns 202 only after both writes succeed and
deletes the object on a failed enqueue. A 14 day lifecycle rule removes
orphans.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bodies are encrypted with AES-256-GCM before they enter the Queue or
R2. The envelope records the algorithm, key id, and IV; payloadSha256
remains the plaintext digest. The delivery id is bound as additional
authenticated data so ciphertext cannot be replayed under another
delivery. The key and key id come from Worker secrets and are validated
as config.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Oversized, non-JSON, and unsupported-event deliveries now return 202
and are dropped with a log line, so GitHub records them as received.
Only infrastructure failures on deliveries Janitor would process return
503. Missing headers and invalid signatures still return 400 and 401.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cluster Worker consumes the GitHub events queue and records each
delivery in a Neon journal table inside one transaction, acknowledging
only after commit or duplicate. Overflow payloads are read from R2 and
deleted after the journal succeeds. Malformed envelopes and missing R2
objects go to a dead-letter queue with content-safe diagnostics. The
journal stores ciphertext; nothing in the consumer decrypts.

Queue and bucket resources move to shared exports so both Workers bind
the same resources. A testcontainers-backed Postgres test covers the
journal store and skips when no container runtime is present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adopt the effect tsgo recommended lint preset, exclude .direnv and
node_modules from test discovery, and inline @effect/sql-pg and
@effect/platform-node in vitest so they share one effect instance with
the tests. The journal test now uses FileSystem and Path from effect
and always expects a container runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The journal transaction also writes an outbox request for
ProjectGitHubWebhookV1 keyed by delivery id. A dispatcher claims due
rows with a bounded lease and fencing token, submits the workflow, and
marks rows accepted; failed submissions release with backoff. The
consumer dispatches after acknowledging and a cron-woken singleton
recovers overdue rows.

The workflow decrypts the journaled payload with the shared key,
decodes it against the domain event schemas, and records projected,
unsupported, or failed on the delivery. Read-model writes come next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Neon tables for installations, repositories, labels, entities, pull
request details, and label assignments. Writes are fenced by GitHub's
updated_at and the journal sequence. The pull request event schema
gains state, merged, updated_at, labels, and the closed action; the
earlier Model classes are replaced by record schemas.

Part of a series; the projection wiring lands with the Worker merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
App JWT signing for PKCS#1 or PKCS#8 keys, cached installation tokens
with one refresh after a 401, and a Neon-backed budget with fenced
leases, per-priority reserves, and cooldowns from Retry-After or an
exhausted limit. Every GitHub call goes through one client that records
rate headers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One target row per scope with generation counters, a five second
debounce, and compare-and-set completion that yields exactly one
follow-up for work that arrives mid-run. The installation inventory
workflow verifies the installation, pages its repositories, marks
unlisted ones suspect, and sleeps on uniquely named durable clocks when
rate limited.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Label catalog scans, incremental issue scans with a watermark and ten
minute overlap, pull request detail scans, and a targeted refresh
workflow that treats a 404 as ambiguous rather than a deletion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A cron-woken planner creates overdue generations per the repair
cadence, staggered per installation, with weekly full repairs. Enabling
a repository bootstraps its tracks. Uninstall or removal schedules a
purge after a seven day grace that wipes ciphertext, titles, bodies,
and label names while keeping identities. A freshness function derives
projected, verified, syncing, stale, or blocked from a target row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stored ETags make scan requests conditional and a 304 serves the stored
page, which validates only that exact representation. A 304 on a page
that was full when stored probes one page further. Cached bodies are
ciphertext bound to their request key and are removed by purges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merge ingress into the one backend Worker the design describes: the
API prefix routes to the webhook layer, probe endpoints stay, and the
standalone webhook Worker is removed. This commit also wires the
series together: projection applies events to the read model,
invalidates sync targets, schedules purges, and records the
installation on each delivery.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The webhooks package only served the cluster Worker, so its code moves
there: ingress routes and verification under Ingress, the queue and
payload store under GitHub, and the payload cipher at the top level.
Service identifiers follow. The topology and migration probes, their
queue, cron, store, migrations, and endpoints are removed now that real
workflows exist; a migration drops the probe table. Database exports are
renamed while Alchemy resource ids stay fixed so live resources are not
replaced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The App private key parser accepts a PEM stored on one line with
literal newline escapes, as .env files hold it. direnv loads .env and
exports the rootless podman variables the Postgres suites need.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Alchemy binds secrets for Config values read while the Worker
initializes; the cluster layer builds lazily, so configs read inside
layers were never bound and every request failed with ConfigError.
The Worker now reads the webhook secret, payload key, and App
credentials during init and hands values to the layers.

Route errors that know their response, such as a malformed request,
now produce that response instead of a 500.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each projected delivery logs its status and sequence so a deployment
can be observed without database access. Malformed requests and
unknown routes log as warnings rather than errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GitHub rejects requests without a User-Agent header with a 403, and Workers
do not add one, so the first installation inventory run failed. Attach
janitor.effectful.co to the cluster Worker to match the App webhook URL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… entity scan

Enabling a repository dispatches all three tracks at once, so the pull request
track inserted details before the entity scan had committed its rows and died
on the foreign key. Skip details for unknown numbers and request a follow-up
generation instead. A run that fails inside the workflow engine leaves no
trace in sync_target, so treat dispatched runs older than thirty minutes as
dead, log every workflow failure and sync outcome, and add operator scripts to
enable a repository and request a track generation against a database.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds GET and POST /api/v1/sync for the re-sync button. The summary derives
from the sync target table; the request invalidates every active scope in one
transaction. The POST trusts only same-origin browsers until Cloudflare Access
verification exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Build the Foldkit app during deploy and attach its output as the cluster
Worker's static assets, so the browser and the API share one origin. The
separate website Worker is gone; non-API paths fall through to the ASSETS
binding with single-page fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The button sits next to the theme switcher, fetches the sync summary on
load, posts a sync-everything request on press, and polls only while a sync
is running. It is disabled with a spinning icon while syncing, the tooltip
shows the last sync time, and toasts announce start, completion, blocked
scopes, and failures. The web app pins the published effect release, so the
wire schema is mirrored locally rather than imported from the domain package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e worker

The cluster Worker enrolls in an Access application that admits members of
the Effectful-Tech GitHub organization, and a hostname-level bypass keeps
the GitHub webhook route outside Access with its signature check. The sync
routes verify the Cf-Access-Jwt-Assertion for signature, issuer, audience,
expiry, and subject against the team's cached signing keys, refreshing once
on an unknown kid. The workers.dev hostname is disabled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Rules carry stable ids, one entity target, the closed concrete predicate
set, and synchronized label references by stable label id. Saves are
whole-ruleset, optimistic on the configured revision, validated against the
labels synchronization knows, and recorded with the Access issuer and
subject as author. GET and PUT on /api/v1/repositories/:id/rules sit behind
the Access assertion check; PUT also requires a same-origin request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@IMax153
IMax153 merged commit a8a9fd0 into main Sep 8, 2026
1 check passed
@IMax153
IMax153 deleted the cluster branch September 8, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant