-
Notifications
You must be signed in to change notification settings - Fork 1
471 lines (422 loc) · 19.7 KB
/
Copy pathjekyll.yml
File metadata and controls
471 lines (422 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: write # Need write access to push Bluesky URI updates back to posts
pages: write
id-token: write
models: read
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build-and-deploy:
name: Build and Deploy
runs-on: ubuntu-latest
outputs:
page-url: ${{ steps.deployment.outputs.page_url }}
new-posts: ${{ steps.detect.outputs.new-posts }}
has-new-posts-without-bluesky: ${{ steps.detect.outputs.has-new-posts-without-bluesky }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect new posts without Bluesky URI
id: detect
run: |
# Get changed files
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
# Find new posts in _posts (only additions, not modifications)
NEW_POSTS=$(git diff --name-only --diff-filter=A HEAD~1 HEAD | grep "^_posts/.*\.md\?$" || true)
echo "New posts: $NEW_POSTS"
# Check if any new posts don't have blueskyPostURL or blueskyPostURI (backward compatibility)
HAS_NEW_POSTS_WITHOUT_BLUESKY="false"
NEW_POSTS_WITHOUT_BLUESKY=""
# Check for posts with forceRepost flag
FORCE_REPOST_POSTS=""
ALL_POSTS=$(find _posts -name "*.md" -type f)
if [ ! -z "$NEW_POSTS" ]; then
for post in $NEW_POSTS; do
if [ -f "$post" ]; then
# Check if post has blueskyPostURL or blueskyPostURI in frontmatter (only between --- markers)
FRONTMATTER=$(awk '/^---$/{if(seen){exit} seen=1; next} seen{print}' "$post")
if ! echo "$FRONTMATTER" | grep -q -E "blueskyPost(URL|URI):"; then
echo "Found new post without Bluesky URL: $post"
HAS_NEW_POSTS_WITHOUT_BLUESKY="true"
if [ -z "$NEW_POSTS_WITHOUT_BLUESKY" ]; then
NEW_POSTS_WITHOUT_BLUESKY="$post"
else
NEW_POSTS_WITHOUT_BLUESKY="$NEW_POSTS_WITHOUT_BLUESKY,$post"
fi
else
echo "Post already has Bluesky URL: $post"
fi
fi
done
fi
# Check for existing posts with forceRepost flag
for post in $ALL_POSTS; do
if [ -f "$post" ]; then
FRONTMATTER=$(awk '/^---$/{if(seen){exit} seen=1; next} seen{print}' "$post")
if echo "$FRONTMATTER" | grep -q "forceRepost:\s*true"; then
echo "Found post with forceRepost flag: $post"
HAS_NEW_POSTS_WITHOUT_BLUESKY="true"
if [ -z "$FORCE_REPOST_POSTS" ]; then
FORCE_REPOST_POSTS="$post"
else
FORCE_REPOST_POSTS="$FORCE_REPOST_POSTS,$post"
fi
fi
fi
done
# Combine new posts and force repost posts
ALL_POSTS_TO_PROCESS=""
if [ ! -z "$NEW_POSTS_WITHOUT_BLUESKY" ]; then
ALL_POSTS_TO_PROCESS="$NEW_POSTS_WITHOUT_BLUESKY"
fi
if [ ! -z "$FORCE_REPOST_POSTS" ]; then
if [ -z "$ALL_POSTS_TO_PROCESS" ]; then
ALL_POSTS_TO_PROCESS="$FORCE_REPOST_POSTS"
else
ALL_POSTS_TO_PROCESS="$ALL_POSTS_TO_PROCESS,$FORCE_REPOST_POSTS"
fi
fi
echo "new-posts=$ALL_POSTS_TO_PROCESS" >> $GITHUB_OUTPUT
echo "has-new-posts-without-bluesky=$HAS_NEW_POSTS_WITHOUT_BLUESKY" >> $GITHUB_OUTPUT
echo "force-repost-posts=$FORCE_REPOST_POSTS" >> $GITHUB_OUTPUT
echo "Has new posts without Bluesky: $HAS_NEW_POSTS_WITHOUT_BLUESKY"
echo "New posts without Bluesky: $NEW_POSTS_WITHOUT_BLUESKY"
echo "Force repost posts: $FORCE_REPOST_POSTS"
echo "All posts to process: $ALL_POSTS_TO_PROCESS"
- name: Setup Ruby
# https://github.com/ruby/setup-ruby/releases/tag/v1.220.0
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 1 # Increment this number if you need to re-download cached gems
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
# Outputs to the './_site' directory by default
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Upload artifact
# Automatically uploads an artifact from the './_site' directory by default
uses: actions/upload-pages-artifact@v3
with:
name: github-pages-initial
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
with:
artifact_name: github-pages-initial
bluesky-social:
name: Post to Bluesky
runs-on: ubuntu-latest
needs: build-and-deploy
if: needs.build-and-deploy.outputs.has-new-posts-without-bluesky == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Process new posts and post to Bluesky
id: process-posts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Install jq for JSON processing
sudo apt-get update && sudo apt-get install -y jq
# Use the correct domain for URL generation
SITE_URL="https://rawlinson.us"
# Initialize variables
BLUESKY_TEXT=""
POST_FILE=""
# Process each new post
IFS=',' read -ra POSTS <<< "${{ needs.build-and-deploy.outputs.new-posts }}"
for post_file in "${POSTS[@]}"; do
echo "Processing $post_file"
# Extract post title and description. maybe qutotes maybe not..
POST_TITLE=$(grep "^title:" "$post_file" | sed 's/title: *//; s/^"\(.*\)"$/\1/')
POST_DESCRIPTION=$(grep "^description:" "$post_file" | sed 's/description: *//; s/^"\(.*\)"$/\1/')
# Extract categories and tags for context
POST_CATEGORY=$(grep "^category:" "$post_file" | sed 's/category: *//')
POST_TAGS=$(grep "^tags:" "$post_file" | sed 's/tags: *//')
# Extract and clean post content (first few paragraphs)
# Remove frontmatter and get first 500 characters of actual content
POST_CONTENT=$(awk '
BEGIN { in_frontmatter = 0; content_started = 0; }
/^---$/ {
if (in_frontmatter == 1) { content_started = 1; }
in_frontmatter = !in_frontmatter;
next;
}
content_started == 1 && in_frontmatter == 0 {
# Skip empty lines at start
if (NF == 0 && length(content) == 0) next;
# Skip markdown headers and links for cleaner text
gsub(/^#+\s*/, "");
gsub(/\[([^\]]*)\]\([^\)]*\)/, "\\1");
gsub(/\*\*([^\*]*)\*\*/, "\\1");
gsub(/\*([^\*]*)\*/, "\\1");
content = content " " $0;
if (length(content) > 500) exit;
}
END { print substr(content, 1, 500); }
' "$post_file")
# Convert filename to URL path (matching Jekyll's behavior)
# Extract filename without date prefix and extension
FILENAME=$(basename "$post_file" .md)
URL_SLUG=$(echo "$FILENAME" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-//')
POST_URL="${SITE_URL}/${POST_CATEGORY}/${URL_SLUG}/#page-title"
# Calculate the exact URL length and determine maximum AI response length
URL_LENGTH=$(echo -n "$POST_URL" | wc -c)
# Reserve 5 characters for the space and any punctuation between message and URL
SPACE_BUFFER=5
MAX_AI_LENGTH=$((300 - URL_LENGTH - SPACE_BUFFER))
# Safety check: ensure we have at least 50 characters for the AI response
if [ "$MAX_AI_LENGTH" -lt 50 ]; then
echo "Warning: URL is very long ($URL_LENGTH chars). Setting minimum AI length to 50."
MAX_AI_LENGTH=50
fi
echo "Post title: $POST_TITLE"
echo "Post description: $POST_DESCRIPTION"
echo "Post URL: $POST_URL"
echo "URL length: $URL_LENGTH characters"
echo "Max AI response length: $MAX_AI_LENGTH characters"
echo "Post category: $POST_CATEGORY"
echo "Post content preview: ${POST_CONTENT:0:200}..."
# Create enhanced prompt with calculated character limit
PROMPT="Write a thoughtful, natural introduction (under $MAX_AI_LENGTH characters) for a blog post share. Create a genuine summary that sparks curiosity through interesting details, not questions or calls to action. Avoid click-bait language, exclamation points, and phrases like 'you need to', 'must read', 'will blow your mind', etc. Write in a conversational, informative tone like you're telling a friend about something interesting you learned. Use US English, avoid em-dashes, and only include hashtags if they naturally fit the sentence flow. The response must be under $MAX_AI_LENGTH characters as a URL will be appended. Title: $POST_TITLE. Description: $POST_DESCRIPTION. Category: $POST_CATEGORY. Content preview: $POST_CONTENT. The URL will be added automatically."
AI_RESPONSE=$(curl -s "https://models.github.ai/inference/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-d "{
\"messages\": [
{
\"role\": \"user\",
\"content\": $(echo "$PROMPT" | jq -R -s .)
}
],
\"model\": \"openai/gpt-4.1\"
}")
echo "ai response: $AI_RESPONSE"
# Check if the response contains an error
API_ERROR=$(echo "$AI_RESPONSE" | jq -r '.error.message // empty')
if [ -n "$API_ERROR" ]; then
echo "❌ GitHub AI API Error: $API_ERROR"
echo "Full error response: $AI_RESPONSE"
AI_MESSAGE="" # Set to empty to trigger fallback
else
# Extract the message from the response
AI_MESSAGE=$(echo "$AI_RESPONSE" | jq -r '.choices[0].message.content // empty')
if [ -z "$AI_MESSAGE" ] || [ "$AI_MESSAGE" = "null" ]; then
echo "❌ AI API returned empty or null content"
echo "Response structure: $AI_RESPONSE"
fi
fi
# Check if AI response is null or empty, use fallbacks
if [ "$AI_MESSAGE" = "null" ] || [ -z "$AI_MESSAGE" ]; then
echo "AI response was null or empty, using fallback logic"
# Check if description has a value
if [ -n "$POST_DESCRIPTION" ] && [ "$POST_DESCRIPTION" != "" ]; then
echo "Using description from frontmatter"
AI_MESSAGE="$POST_DESCRIPTION"
else
# Check if excerpt has a value
POST_EXCERPT=$(grep "^excerpt:" "$post_file" | sed 's/excerpt: *//; s/^"\(.*\)"$/\1/')
if [ -n "$POST_EXCERPT" ] && [ "$POST_EXCERPT" != "" ]; then
echo "Using excerpt from frontmatter"
AI_MESSAGE="$POST_EXCERPT"
else
# Use first 200 characters of clean text content
echo "Using first 200 characters of post content"
CLEAN_CONTENT=$(echo "$POST_CONTENT" | sed 's/!\[[^\]]*\]([^)]*)//g; s/\[[^\]]*\]([^)]*)//g; s/\*\*//g; s/\*//g; s/#\+//g' | tr -s ' ' | sed 's/^ *//; s/ *$//')
AI_MESSAGE=$(echo "$CLEAN_CONTENT" | cut -c1-200)
fi
fi
fi
BLUESKY_TEXT="${AI_MESSAGE} ${POST_URL}"
POST_FILE="$post_file"
echo "Generated message: $BLUESKY_TEXT"
# Set outputs for next step
echo "BLUESKY_TEXT=$BLUESKY_TEXT" >> $GITHUB_OUTPUT
echo "POST_FILE=$POST_FILE" >> $GITHUB_OUTPUT
# Only process one post for now
break
done
- name: Post to Bluesky
if: steps.process-posts.outputs.BLUESKY_TEXT != ''
id: bluesky-post
continue-on-error: true
uses: finalcut/bluesky-post-action@feature/add-post-outputs
env:
BSKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }}
BSKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
with:
post: ${{ steps.process-posts.outputs.BLUESKY_TEXT }}
service: https://bsky.social
- name: Check Bluesky post result and analyze errors
if: steps.process-posts.outputs.BLUESKY_TEXT != ''
run: |
POST_TEXT="${{ steps.process-posts.outputs.BLUESKY_TEXT }}"
if [ "${{ steps.bluesky-post.outcome }}" = "failure" ]; then
echo "❌ Bluesky posting failed!"
# Count characters using standard Unix tools
# AWK length() function handles UTF-8 properly and is closest to grapheme count
CHAR_COUNT=$(echo -n "$POST_TEXT" | wc -c)
UNICODE_CHAR_COUNT=$(echo -n "$POST_TEXT" | wc -m 2>/dev/null || echo -n "$POST_TEXT" | wc -c)
AWK_CHAR_COUNT=$(echo -n "$POST_TEXT" | awk '{print length($0)}')
echo "📊 Message analysis:"
echo " Character count (awk): $AWK_CHAR_COUNT"
echo " Unicode chars (wc -m): $UNICODE_CHAR_COUNT"
echo " Bytes (wc -c): $CHAR_COUNT"
echo " Bluesky limit: 300 graphemes"
echo ""
echo "📝 Message content:"
echo "$POST_TEXT"
echo ""
# Use AWK count as best approximation for graphemes
if [ "$AWK_CHAR_COUNT" -gt 300 ]; then
echo "💡 The message is $(($AWK_CHAR_COUNT - 300)) characters over the 300 limit."
echo " Consider shortening the AI prompt to generate shorter messages."
fi
echo "::error title=Bluesky Post Failed::Message has $AWK_CHAR_COUNT characters (limit: 300). Check the AI prompt length."
exit 1
else
echo "✅ Bluesky posting succeeded!"
echo " URL: ${{ steps.bluesky-post.outputs.url }}"
fi
- name: Update post with Bluesky URL
if: steps.bluesky-post.outputs.url != ''
id: update-posts-in-github
run: |
# Update the post file with the Bluesky URL
POST_FILE="${{ steps.process-posts.outputs.POST_FILE }}"
BLUESKY_URL="${{ steps.bluesky-post.outputs.url }}"
# Check if this was a force repost
IS_FORCE_REPOST="false"
if grep -q "forceRepost:\s*true" "$POST_FILE"; then
IS_FORCE_REPOST="true"
echo "This is a force repost - will remove forceRepost flag"
fi
# Check if post already has blueskyPostURL
if grep -q "blueskyPostURL:" "$POST_FILE"; then
echo "Post already has blueskyPostURL. Replacing existing entries."
# Remove all existing blueskyPostURL lines and add the new one
awk -v uri="$BLUESKY_URL" -v remove_force_repost="$IS_FORCE_REPOST" '
BEGIN { in_frontmatter = 0; inserted = 0 }
/^---$/ {
if (!in_frontmatter) {
in_frontmatter = 1
print $0
} else if (!inserted) {
print "blueskyPostURL: " uri
print $0
inserted = 1
} else {
print $0
}
next
}
# Skip existing blueskyPostURL lines
/^blueskyPostURL:/ && in_frontmatter { next }
# Skip forceRepost lines if this was a force repost
/^forceRepost:/ && in_frontmatter && remove_force_repost == "true" { next }
{ print $0 }
' "$POST_FILE" > "$POST_FILE.tmp" && mv "$POST_FILE.tmp" "$POST_FILE"
else
# Add new blueskyPostURL to frontmatter
echo "Adding new blueskyPostURL to frontmatter."
awk -v uri="$BLUESKY_URL" -v remove_force_repost="$IS_FORCE_REPOST" '
BEGIN { in_frontmatter = 0; inserted = 0 }
/^---$/ {
if (!in_frontmatter) {
in_frontmatter = 1
print $0
} else if (!inserted) {
print "blueskyPostURL: " uri
print $0
inserted = 1
} else {
print $0
}
next
}
# Skip forceRepost lines if this was a force repost
/^forceRepost:/ && in_frontmatter && remove_force_repost == "true" { next }
{ print $0 }
' "$POST_FILE" > "$POST_FILE.tmp" && mv "$POST_FILE.tmp" "$POST_FILE"
fi
echo "Updated $POST_FILE with Bluesky URL: $BLUESKY_URL"
if [ "$IS_FORCE_REPOST" = "true" ]; then
echo "Removed forceRepost flag from $POST_FILE"
fi
- name: Commit updated post files
if: steps.bluesky-post.outputs.url != ''
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add _posts/
# Check if any files were changed
if ! git diff --staged --quiet; then
# Check if this was a force repost
if [[ "${{ needs.build-and-deploy.outputs.new-posts }}" == *"forceRepost"* ]]; then
git commit -m "Update Bluesky post URL and remove forceRepost flag"
else
git commit -m "Add Bluesky post URL to new blog post"
fi
git push
else
echo "No changes to commit"
fi
rebuild-after-bluesky:
name: Rebuild After Bluesky Update
runs-on: ubuntu-latest
needs: bluesky-social
if: always() && needs.bluesky-social.result == 'success'
steps:
- name: Checkout updated code
uses: actions/checkout@v4
with:
ref: main
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1'
bundler-cache: true
cache-version: 1
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
name: github-pages-final
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
with:
artifact_name: github-pages-final