generated from alex/base-template
Plan: self-contained Deno app mirroring PDS avatar → Gravatar, running as podman #1
Labels
No labels
dependencies
No milestone
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
alex/atproto-gravatar-sync#1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Keep the account's Gravatar profile picture in sync with its Bluesky/ATProto avatar. This re-thinks the sync (originally explored in
shaftoe/personal-website#401, then prototyped as an Ansible role inalex/ansible#50) as a standalone Deno project running as a podman container, developed in isolation here.The core sync logic is already proven in the Ansible role (
gravatar-avatar-sync.tsinalex/ansible#50). This repo extracts it into a self-contained project: own Containerfile, own CI, own scheduling, no Ansible dependency.How the sync works
A single Deno run does, in order:
com.atproto.identity.resolveHandle).app.bsky.actor.profile) → avatar blob cid + mime type.com.atproto.sync.getBlob) and upload to Gravatar (POST /me/avatars).POST /me/avatars/{id}/emailwithSHA-256(normalised email)).selectedflag can never cause it to delete the picture it just uploaded.The PDS reads are public xrpc calls (no PDS credentials needed); only the Gravatar token + email are secret.
Runtime shape — podman
Systemd timer →
podman run(recommended)gravatar-avatar-sync.serviceis aType=oneshotthat doespodman run --rm forge.l3x.in/alex/atproto-gravatar-sync.gravatar-avatar-sync.timerfires it on a schedule (Persistent=true, daily).--env-file; the state file persists on a mounted volume. The container is ephemeral (--rm).This matches the PDS host's existing Quadlet/podman patterns and keeps scheduling on systemd (where the PDS host already manages timers).
Proposed project layout
Configuration (env vars)
All config flows in through the environment (no secrets baked into the image):
PDS_URLhttps://pds.example.com)HANDLEGRAVATAR_TOKENgravatar-profile:managescope)GRAVATAR_EMAILGRAVATAR_API_URLhttps://api.gravatar.com/v3STATE_FILE/data/gravatar-sync.stateImplementation plan
deno.json(tasks, lint, fmt),main.ts(port the existinggravatar-avatar-sync.tsverbatim as a starting point),.forgejo/workflows/ci.yml.selectedflag.denoland/deno:2.xbase, copy source, runmain.ts. Keep the image minimal; considerdeno compilefor a single static binary in ascratch/distroless final image..service(oneshot,podman run --rm) +.timer(daily,Persistent=true) undersystemd/.deno check/fmt --check/lint,deno test, and a Containerfile build step.Notes
fetch,crypto.subtle,FormData,Blob). Keep it that way; no external HTTP/CLI dependencies.--rmcontainers this is the only thing that needs to persist./pi new pr
I'll implement the full plan from issue #1. Let me first check the environment and existing structure.No Deno locally. Let me install it so I can validate the code (fmt/lint/check/test) before pushing.Deno 2.1.4 installed. Now let me create all the project files.Now the main entry point that orchestrates everything:Now the test suite with a mocked PDS + Gravatar HTTP layer:Let me fix the
loadConfigtest — the lazy import approach won't catch the throw correctly. I'll import it directly at the top.Now let me run the checks: format, lint, type check, and tests.Several issues to fix: thesingleQuoteconfig fights my double-quoted code, thenamefield warns, the fake-fetchasynctriggersrequire-await, and the env test needs permissions. Let me refactorloadConfigto accept an env getter (cleaner, no permissions needed), fix the fake fetch, and simplifydeno.json.Now fix the fake fetch (removeasync/require-await) and rewrite the env tests to use the injected getter:Now rundeno fmtto normalize formatting, then re-run all checks:All green: fmt, lint, check, and 7/7 tests pass. Now let me verifydeno compileworks and generate the lockfile:Binary works and exits correctly (code 1 on error). Zero external deps confirmed — nodeno.lockneeded. Now the infrastructure files: Containerfile, compose, systemd units, CI, and README.Now the README and a.gitignore:Let me run a final check pass on all files and attempt the container build (docker is available):The docker failure is a sandbox limitation (buildkit needscatatonit), not a Containerfile problem — thedeno compilestep already validated successfully locally. Let me fix the README formatting issue:All formatted. Final full verification pass:Everything passes. The systemd verify only warns thatpodmanisn't installed in this sandbox (expected), not a syntax error. Let me do a final review of the key files:Everything is in place and validated. Let me create the PR:PR #2 created: #2Closes #1. The implementation is complete and fully validated locally:
deno fmt --check/deno lint/deno check— all cleandeno test— 7/7 passing (fresh upload+activate, skip on unchanged, re-upload+prune-old, no-op on avatar removed, flaky-selectedsafety, andloadConfigvalidation)deno compile— produces a standalone binary that exits cleanly with a clear message on missing envThe sync port preserves the proven flow (resolve → read cid → skip-if-unchanged → fetch/upload/activate → identity-protected cleanup → persist), is zero-dependency (so no lockfile), and ships with a multi-stage
Containerfile, template systemd units, a compose file,.env.example, README, and a CI workflow (the existingpi.ymlis untouched).View action run | Model: zai/glm-5.2 (thinking: xhigh) | Time: 10m 3s | Tokens: 960.4K | Pi SDK v0.80.3 | Action v2.25.1-develop (develop @ 5c04899)