audio streaming app plyr.fm

ops: bsky DM on deploy failure (#1027)

* ops: bsky DM on deploy failure

prod deploy v284 silently failed and went unnoticed for ~5h. add a
self-contained notification script that sends a bsky DM when the deploy
workflow fails, using the same atproto DM pattern as the backend.

- scripts/notify_deploy_failure.py: standalone script (no backend deps)
- deploy-prod.yml: if: failure() step runs via uvx --with atproto

requires 3 GHA secrets: BSKY_NOTIFY_HANDLE, BSKY_NOTIFY_PASSWORD,
BSKY_NOTIFY_RECIPIENT

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add setup-uv step before uvx notify script

uvx isn't available on GHA runners by default — need
astral-sh/setup-uv first, matching other workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: reuse existing NOTIFY_BOT_* / NOTIFY_RECIPIENT_HANDLE secrets

these are already set in GHA — no need for new BSKY_NOTIFY_* secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by zzstoatzz.io

Claude Opus 4.6 and committed by
GitHub
bcc79c1e 18ba8b01

+57
+12
.github/workflows/deploy-prod.yml
··· 28 flyctl deploy --config backend/fly.toml --remote-only -a relay-api . 29 env: 30 FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PROD }}
··· 28 flyctl deploy --config backend/fly.toml --remote-only -a relay-api . 29 env: 30 FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PROD }} 31 + 32 + - name: install uv 33 + if: failure() 34 + uses: astral-sh/setup-uv@v7 35 + 36 + - name: notify deploy failure 37 + if: failure() 38 + run: uvx --with atproto python scripts/notify_deploy_failure.py 39 + env: 40 + NOTIFY_BOT_HANDLE: ${{ secrets.NOTIFY_BOT_HANDLE }} 41 + NOTIFY_BOT_PASSWORD: ${{ secrets.NOTIFY_BOT_PASSWORD }} 42 + NOTIFY_RECIPIENT_HANDLE: ${{ secrets.NOTIFY_RECIPIENT_HANDLE }}
+45
scripts/notify_deploy_failure.py
···
··· 1 + """send a bsky DM when a deploy fails.""" 2 + 3 + import os 4 + 5 + from atproto import Client, models 6 + 7 + 8 + def main() -> None: 9 + handle = os.environ["NOTIFY_BOT_HANDLE"] 10 + password = os.environ["NOTIFY_BOT_PASSWORD"] 11 + recipient = os.environ["NOTIFY_RECIPIENT_HANDLE"] 12 + 13 + # context from GHA 14 + run_id = os.environ.get("GITHUB_RUN_ID", "unknown") 15 + repo = os.environ.get("GITHUB_REPOSITORY", "unknown") 16 + ref = os.environ.get("GITHUB_REF_NAME", "unknown") 17 + 18 + client = Client() 19 + client.login(handle, password) 20 + 21 + # resolve recipient handle to DID via the API 22 + profile = client.app.bsky.actor.get_profile({"actor": recipient}) 23 + recipient_did = profile.did 24 + 25 + dm_client = client.with_bsky_chat_proxy() 26 + dm = dm_client.chat.bsky.convo 27 + 28 + convo = dm.get_convo_for_members( 29 + models.ChatBskyConvoGetConvoForMembers.Params(members=[recipient_did]) 30 + ).convo 31 + 32 + url = f"https://github.com/{repo}/actions/runs/{run_id}" 33 + message = f"deploy failed on plyr.fm\n\nref: {ref}\nrun: {url}" 34 + 35 + dm.send_message( 36 + models.ChatBskyConvoSendMessage.Data( 37 + convo_id=convo.id, 38 + message=models.ChatBskyConvoDefs.MessageInput(text=message), 39 + ) 40 + ) 41 + print(f"sent deploy failure DM to @{recipient}") 42 + 43 + 44 + if __name__ == "__main__": 45 + main()