Skip to content

[3/4] feat: linkedin-sync worker that debounces profile changes into deploys - #5

Open
devnull03 wants to merge 2 commits into
stack/2-static-buildfrom
stack/3-linkedin-sync-worker
Open

devnull03 wants to merge 2 commits into
stack/2-static-buildfrom
stack/3-linkedin-sync-worker

Conversation

@devnull03

Copy link
Copy Markdown
Owner

Stacked on #4. Next: #6.

LinkedIn has no "profile changed" webhook, so workers/linkedin-sync is a standalone Cloudflare Worker. It collects change signals and turns them into one redeploy once they have been quiet for 2 hours.

 POST /hook ───────────┐
 PUT  /export (zip) ───┼──► Debouncer (Durable Object)        content-release.yml (#4)
 hourly cron (DMA) ────┘    alarm = last signal + 2h  ─────►  repository_dispatch ──► tag ──► deploy

How it works

  • The debounce is a Durable Object alarm. Every signal pushes the alarm out to now + DEBOUNCE_MINUTES (120). MAX_WAIT_MINUTES (24 h) stops a constant trickle of edits from postponing forever. When the alarm fires, the worker sends repository_dispatch: content-changed with the collected reasons.
  • A failed dispatch stays pending, and the alarm retries with backoff.
  • A signal that arrives mid-dispatch isn't lost.
  • Signals
    • POST /hook: an iOS Shortcut, a mail rule on LinkedIn's notification emails, or curl.
    • PUT /export: upload the LinkedIn data-export ZIP. It is stored in R2 and served to CI via GET /export with a separate read-only token, so content:fetch builds from it. For a non-EEA account this is how new LinkedIn data reaches the site. Re-uploading an identical file is ignored.
    • Hourly cron: only when LINKEDIN_DMA_TOKEN is set (EEA/CH accounts). It hashes the official Snapshot API data canonically and signals on change; the first hash is a baseline.
  • GET /status shows what's pending and when it fires, plus the last dispatch result. POST /flush deploys now.
  • Tokens are compared in constant time. ?token= also works for senders that can't set headers.

Setup

See workers/linkedin-sync/README.md:

  1. wrangler r2 bucket create linkedin-exports
  2. Worker secrets: HOOK_SECRET, EXPORT_READ_TOKEN, and GITHUB_TOKEN (a fine-grained PAT with Contents: read & write on this repo, which repository_dispatch requires).
  3. pnpm worker:deploy
  4. In this repo's settings: LINKEDIN_EXPORT_URL (variable) and LINKEDIN_EXPORT_TOKEN (secret).

Verification

  • pnpm worker:test: 6 node --test cases for the debounce state machine (window reset, max-wait cap, single dispatch, retry on failure, mid-dispatch signal, fingerprint baseline).
  • pnpm worker:check: tsc against generated Workers types. Both run in CI.
  • End to end in local wrangler dev, with a 3 s debounce and a mock GitHub API:
    • 3 rapid hooks → exactly 1 dispatch after the window, carrying all 3 reasons.
    • Export upload → stored, then dispatched; the same ZIP again → nothing scheduled; a non-ZIP body → 400.
    • The read-only token can download the export but can't trigger a deploy (401).
    • LINKEDIN_EXPORT=<worker>/export pnpm content:fetch parses the uploaded ZIP.
    • Unauthenticated requests → 401.

Generated by Claude Code

…ploys

LinkedIn has no profile-changed webhook, so a standalone Cloudflare
Worker (workers/linkedin-sync) collects change signals and turns them
into a single redeploy once they have been quiet for 2 hours.

- One Durable Object owns the debounce timer: every signal pushes its
  alarm out to now + DEBOUNCE_MINUTES (120), capped by MAX_WAIT_MINUTES
  (24h). When the alarm fires it sends repository_dispatch
  content-changed, which runs content-release.yml. A failed dispatch
  stays pending and the alarm retries with backoff.
- Signals: POST /hook (Shortcut, mail rule, curl), PUT /export (upload a
  LinkedIn export ZIP, kept in R2 and served to CI via GET /export with
  a read-only token), and an hourly cron that hashes the DMA Snapshot
  API data when LINKEDIN_DMA_TOKEN is set (EEA/CH accounts only).
- GET /status and POST /flush for visibility and manual deploys.
- Debounce logic is runtime-agnostic and unit tested with node --test;
  CI runs the tests and a type-check against generated Workers types.
Copilot AI balanced review requested due to automatic review settings October 6, 2026 05:53
@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portfolio Ready Ready Preview Oct 6, 2026 6:06am UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch was successfully deployed

1 active deployment
Preview — a47ebaf5 Deployed Oct 6, 2026 by vercel[bot]
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.

2 participants