Skip to content

Commit 6b92207

Browse files
authored
Add workflow to post new releases on Bluesky (#1439)
1 parent 4d156c3 commit 6b92207

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

.github/workflows/bluesky.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Bluesky
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
post:
12+
name: Post to Bluesky
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v7
20+
21+
- name: Install mise
22+
uses: jdx/mise-action@v4
23+
24+
- name: Post release thread to Bluesky
25+
env:
26+
BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }}
27+
BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
28+
RELEASE_TAG: ${{ github.event.release.tag_name }}
29+
RELEASE_URL: ${{ github.event.release.html_url }}
30+
RELEASE_BODY: ${{ github.event.release.body }}
31+
run: mise run post-to-bluesky

.mise/tasks/post-to-bluesky

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.10"
4+
# dependencies = [
5+
# "atproto>=0.0.55",
6+
# ]
7+
# ///
8+
# MISE description="Post a release announcement thread to Bluesky"
9+
10+
"""Post a TorchIO release announcement as a threaded post on Bluesky.
11+
12+
Expects the following environment variables:
13+
BLUESKY_USERNAME: Bluesky handle (e.g. torchio.org)
14+
BLUESKY_PASSWORD: Bluesky App Password
15+
RELEASE_TAG: Git tag for the release (e.g. v1.2.0)
16+
RELEASE_URL: URL to the GitHub release page
17+
RELEASE_BODY: Markdown body of the release notes
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import os
23+
import re
24+
import sys
25+
26+
from atproto import Client, client_utils
27+
28+
29+
def get_env(name: str) -> str:
30+
"""Get a required environment variable or exit with an error."""
31+
value = os.environ.get(name)
32+
if not value:
33+
print(f"Error: {name} environment variable is required", file=sys.stderr)
34+
sys.exit(1)
35+
return value
36+
37+
38+
def extract_bullet_points(body: str) -> list[str]:
39+
"""Extract bullet points from a Markdown release body.
40+
41+
Args:
42+
body: Markdown text containing bullet points.
43+
44+
Returns:
45+
List of bullet point strings with the leading marker removed.
46+
"""
47+
bullets = []
48+
for line in body.splitlines():
49+
match = re.match(r"^\s*[-*]\s+(.+)$", line)
50+
if match:
51+
bullets.append(match.group(1).strip())
52+
return bullets
53+
54+
55+
def build_root_text(tag: str, url: str) -> client_utils.TextBuilder:
56+
"""Build the root post text with a clickable link facet.
57+
58+
Args:
59+
tag: The release tag (e.g. v1.2.0).
60+
url: The URL to the GitHub release page.
61+
62+
Returns:
63+
A TextBuilder with the root post content.
64+
"""
65+
tb = client_utils.TextBuilder()
66+
tb.text(f"TorchIO {tag} is out!\n\n")
67+
tb.link(url, url)
68+
return tb
69+
70+
71+
def main() -> None:
72+
username = get_env("BLUESKY_USERNAME")
73+
password = get_env("BLUESKY_PASSWORD")
74+
tag = get_env("RELEASE_TAG")
75+
url = get_env("RELEASE_URL")
76+
body = os.environ.get("RELEASE_BODY", "")
77+
78+
client = Client()
79+
client.login(username, password)
80+
print(f"Logged in as {username}")
81+
82+
root_text = build_root_text(tag, url)
83+
root_post = client.send_post(root_text)
84+
print(f"Root post created: {root_post.uri}")
85+
86+
bullets = extract_bullet_points(body)
87+
if not bullets:
88+
print("No bullet points found in release notes; thread complete.")
89+
return
90+
91+
root_ref = client.com.atproto.repo.StrongRef(
92+
uri=root_post.uri,
93+
cid=root_post.cid,
94+
)
95+
parent_ref = root_ref
96+
97+
for i, bullet in enumerate(bullets, 1):
98+
reply_ref = client.app.bsky.feed.post.ReplyRef(
99+
root=root_ref,
100+
parent=parent_ref,
101+
)
102+
reply_post = client.send_post(
103+
text=bullet,
104+
reply_to=reply_ref,
105+
)
106+
parent_ref = client.com.atproto.repo.StrongRef(
107+
uri=reply_post.uri,
108+
cid=reply_post.cid,
109+
)
110+
print(f"Reply {i}/{len(bullets)} created: {reply_post.uri}")
111+
112+
print(f"Thread complete: {len(bullets)} replies posted.")
113+
114+
115+
if __name__ == "__main__":
116+
main()

0 commit comments

Comments
 (0)