Skip to content

Commit 321486a

Browse files
authored
Merge pull request #1 from CodingWithCalvin/feature/bluesky-reusable-workflow
Add reusable Bluesky posting workflow
2 parents 582a7a6 + 9bda1ea commit 321486a

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed

.github/workflows/bluesky-post.yml

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Post to BlueSky
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
post_text:
7+
description: 'The text content of the post. Supports markdown links [text](url) and hashtags.'
8+
required: true
9+
type: string
10+
secrets:
11+
BLUESKY_USERNAME:
12+
required: true
13+
BLUESKY_APP_PASSWORD:
14+
required: true
15+
16+
jobs:
17+
post:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Post to BlueSky
21+
run: |
22+
# Authenticate with BlueSky
23+
echo "Authenticating with BlueSky..."
24+
AUTH_RESPONSE=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
25+
-H "Content-Type: application/json" \
26+
-d "{\"identifier\": \"${{ secrets.BLUESKY_USERNAME }}\", \"password\": \"${{ secrets.BLUESKY_APP_PASSWORD }}\"}")
27+
28+
ACCESS_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.accessJwt')
29+
DID=$(echo "$AUTH_RESPONSE" | jq -r '.did')
30+
31+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then
32+
echo "Error: Failed to authenticate with BlueSky"
33+
echo "Response: $AUTH_RESPONSE"
34+
exit 1
35+
fi
36+
37+
echo "✓ Authenticated as $DID"
38+
39+
# Get current timestamp in ISO 8601 format
40+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
41+
42+
# Parse markdown links and calculate facets using Python
43+
echo "Parsing markdown and calculating facets..."
44+
export INPUT_TEXT="${{ inputs.post_text }}"
45+
46+
RESULT=$(python3 << 'PYEOF'
47+
import json, re, os
48+
49+
input_text = os.environ['INPUT_TEXT']
50+
51+
# Parse markdown links [text](url) and build facets
52+
facets = []
53+
output_text = ''
54+
last_end = 0
55+
56+
# Find all markdown links
57+
md_links = list(re.finditer(r'\[([^\]]+)\]\(([^)]+)\)', input_text))
58+
59+
for m in md_links:
60+
# Add text before this link
61+
output_text += input_text[last_end:m.start()]
62+
link_text = m.group(1)
63+
link_url = m.group(2)
64+
65+
# Calculate byte position in output text
66+
byte_start = len(output_text.encode('utf-8'))
67+
output_text += link_text
68+
byte_end = len(output_text.encode('utf-8'))
69+
70+
facets.append({
71+
'index': {'byteStart': byte_start, 'byteEnd': byte_end},
72+
'features': [{'$type': 'app.bsky.richtext.facet#link', 'uri': link_url}]
73+
})
74+
75+
last_end = m.end()
76+
77+
# Add remaining text
78+
output_text += input_text[last_end:]
79+
80+
# Add hashtag facets
81+
for m in re.finditer(r'#(\w+)', output_text):
82+
facets.append({
83+
'index': {'byteStart': len(output_text[:m.start()].encode('utf-8')), 'byteEnd': len(output_text[:m.end()].encode('utf-8'))},
84+
'features': [{'$type': 'app.bsky.richtext.facet#tag', 'tag': m.group(1)}]
85+
})
86+
87+
# Add bare URL facets
88+
for m in re.finditer(r'https?://[^\s]+', output_text):
89+
byte_start = len(output_text[:m.start()].encode('utf-8'))
90+
byte_end = len(output_text[:m.end()].encode('utf-8'))
91+
# Skip if this position overlaps with an existing link facet
92+
already_linked = any(
93+
f['index']['byteStart'] <= byte_start and f['index']['byteEnd'] >= byte_end
94+
for f in facets if any(feat.get('uri') for feat in f['features'])
95+
)
96+
if not already_linked:
97+
facets.append({
98+
'index': {'byteStart': byte_start, 'byteEnd': byte_end},
99+
'features': [{'$type': 'app.bsky.richtext.facet#link', 'uri': m.group()}]
100+
})
101+
102+
# Output as JSON object with both text and facets
103+
print(json.dumps({'text': output_text, 'facets': facets}))
104+
PYEOF
105+
)
106+
107+
POST_TEXT=$(echo "$RESULT" | jq -r '.text')
108+
FACETS=$(echo "$RESULT" | jq -c '.facets')
109+
110+
echo "Post text: $POST_TEXT"
111+
echo "Facets: $FACETS"
112+
113+
# Create the post using jq to properly escape JSON
114+
echo "Creating BlueSky post..."
115+
POST_RESPONSE=$(jq -n \
116+
--arg did "$DID" \
117+
--arg text "$POST_TEXT" \
118+
--arg timestamp "$TIMESTAMP" \
119+
--argjson facets "$FACETS" \
120+
'{
121+
repo: $did,
122+
collection: "app.bsky.feed.post",
123+
record: {
124+
text: $text,
125+
facets: $facets,
126+
createdAt: $timestamp,
127+
"$type": "app.bsky.feed.post"
128+
}
129+
}' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
130+
-H "Content-Type: application/json" \
131+
-H "Authorization: Bearer $ACCESS_TOKEN" \
132+
-d @-)
133+
134+
POST_URI=$(echo "$POST_RESPONSE" | jq -r '.uri')
135+
136+
if [ -z "$POST_URI" ] || [ "$POST_URI" == "null" ]; then
137+
echo "Error: Failed to create BlueSky post"
138+
echo "Response: $POST_RESPONSE"
139+
exit 1
140+
fi
141+
142+
# Extract the post ID from the URI
143+
POST_ID=$(echo "$POST_URI" | sed 's|.*/||')
144+
POST_URL="https://bsky.app/profile/${{ secrets.BLUESKY_USERNAME }}/post/$POST_ID"
145+
146+
echo "✓ Posted to BlueSky: $POST_URL"
147+
shell: bash

0 commit comments

Comments
 (0)