Skip to content

Commit 9ca80c3

Browse files
authored
Merge pull request #61 from VoynichLabs/feature/buildinpublic-twitter-automation
docs: proposal 60 - automated #buildinpublic Twitter posting from GitHub commits
2 parents a04c830 + cd275bb commit 9ca80c3

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Automated #BuildInPublic Twitter Posting from GitHub Commits
2+
3+
**Status:** Draft
4+
**Author:** Mark (Egon/VoynichLabs)
5+
**Date:** 2026-02-21
6+
**Category:** Developer Visibility / Growth
7+
8+
---
9+
10+
## Summary
11+
12+
Automate the posting of `#buildinpublic` tweets directly from PlanExeOrg/PlanExe GitHub commits — no human required in the loop. A cron job polls for new commits, passes the diff/message to an LLM to generate a short tweet, and posts it via the Twitter API. The goal is passive discoverability without asking Simon or Mark to manually post social updates.
13+
14+
---
15+
16+
## Problem
17+
18+
Neither Simon nor Mark wants to manually maintain a social media presence for PlanExe. But steady, technical #buildinpublic updates are one of the most effective organic discovery signals for developer-focused open source projects. The gap: there's genuine daily progress happening in commits, and zero signal going out to Twitter.
19+
20+
This proposal closes that gap with zero ongoing human effort.
21+
22+
---
23+
24+
## Concept
25+
26+
```
27+
PlanExeOrg/PlanExe commits
28+
29+
30+
Cron job polls GitHub API
31+
(checks since last_commit_sha)
32+
33+
34+
LLM generates tweet
35+
(technical, not marketing fluff)
36+
37+
38+
Post via Twitter API / bird CLI
39+
(on designated account)
40+
41+
42+
#buildinpublic feed
43+
```
44+
45+
Key constraint: **fully automated, no human approval step**. The value is in the consistency and zero-friction. If humans need to approve each tweet, it will rot.
46+
47+
---
48+
49+
## Why This Works
50+
51+
1. **Commits already describe what changed** — the signal is already there; this just redistributes it.
52+
2. **#buildinpublic audience is technical** — they want to see actual work, not marketing copy.
53+
3. **LLM-generated summaries scale** — one prompt template handles all commit types gracefully.
54+
4. **Low risk** — if the bot posts something awkward, it's a minor inconvenience, not a crisis. The output is technical commit notes, not opinions.
55+
56+
---
57+
58+
## Architecture
59+
60+
### 1. GitHub API Polling
61+
62+
Poll the `PlanExeOrg/PlanExe` commits endpoint:
63+
64+
```
65+
GET https://api.github.com/repos/PlanExeOrg/PlanExe/commits?since=<ISO_TIMESTAMP>
66+
```
67+
68+
- Store last-processed commit SHA or timestamp in a state file
69+
- On each run: fetch commits since last state, process newest-first or oldest-first (TBD)
70+
- Skip merge commits (configurable)
71+
72+
### 2. State File
73+
74+
```
75+
~/.planexe_twitter_bot/last_commit_sha.txt
76+
```
77+
78+
Stores the SHA of the last successfully tweeted commit. On next run, fetch all commits after this SHA. Prevents duplicate tweets. If missing, use a fixed start date.
79+
80+
### 3. LLM Tweet Generation
81+
82+
Pass commit metadata to a small LLM (Claude haiku / GPT-4o-mini / Gemini Flash — cheapest available):
83+
84+
**Prompt template:**
85+
86+
```
87+
You are generating a short #buildinpublic tweet for an open source AI planning tool.
88+
89+
Repository: PlanExe (AI-powered project planning)
90+
Commit: {commit_sha[:7]}
91+
Message: {commit_message}
92+
Files changed: {changed_files_summary}
93+
Author: {author_name}
94+
95+
Write a tweet under 240 characters. Rules:
96+
- Technical, factual tone — describe what actually changed
97+
- No exclamation marks, no hype, no "excited to announce"
98+
- Include the GitHub commit URL
99+
- End with #buildinpublic #opensource
100+
- If the commit is a tiny fix (typo, whitespace), say so honestly
101+
102+
Tweet:
103+
```
104+
105+
### 4. Posting
106+
107+
**Option A — bird CLI** (if Mark's account):
108+
109+
```bash
110+
bird tweet post "<generated_tweet>"
111+
```
112+
113+
**Option B — Twitter API credentials** (if Egon account or dedicated @PlanExeBot):
114+
115+
```bash
116+
curl -X POST https://api.twitter.com/2/tweets \
117+
-H "Authorization: Bearer $TWITTER_BEARER_TOKEN" \
118+
-H "Content-Type: application/json" \
119+
-d '{"text": "<generated_tweet>"}'
120+
```
121+
122+
**Option C — twurl / tweepy** (Python script with env-var credentials)
123+
124+
### 5. Cron Schedule Options
125+
126+
| Mode | Schedule | Tweet volume | Notes |
127+
|------|----------|-------------|-------|
128+
| Per-commit | On every commit push (webhook or 15-min poll) | High | Most responsive; noisy on busy days |
129+
| Daily digest | Once/day at 09:00 UTC | 1/day max | Summarise all commits from past 24h |
130+
| Weekly summary | Monday 09:00 UTC | 1/week | Lowest noise; best for slow periods |
131+
132+
**Recommendation:** Start with daily digest. Reduces noise, allows batching, and a once-per-day tweet is sustainable even on quiet days (just posts nothing if no commits).
133+
134+
---
135+
136+
## Implementation Sketch (Pseudocode)
137+
138+
```bash
139+
#!/bin/bash
140+
# planexe_twitter_bot.sh — daily digest mode
141+
142+
STATE_FILE="$HOME/.planexe_twitter_bot/last_run_timestamp.txt"
143+
REPO="PlanExeOrg/PlanExe"
144+
GH_TOKEN="$GITHUB_TOKEN"
145+
146+
# 1. Read last run timestamp (default: 24h ago)
147+
if [ -f "$STATE_FILE" ]; then
148+
SINCE=$(cat "$STATE_FILE")
149+
else
150+
SINCE=$(date -u -d "24 hours ago" +%Y-%m-%dT%H:%M:%SZ)
151+
fi
152+
153+
# 2. Fetch commits since last run
154+
COMMITS=$(curl -s \
155+
-H "Authorization: token $GH_TOKEN" \
156+
"https://api.github.com/repos/$REPO/commits?since=$SINCE&per_page=50")
157+
158+
COMMIT_COUNT=$(echo "$COMMITS" | jq length)
159+
160+
if [ "$COMMIT_COUNT" -eq 0 ]; then
161+
echo "No new commits. Skipping."
162+
exit 0
163+
fi
164+
165+
# 3. Build summary for LLM
166+
SUMMARY=$(echo "$COMMITS" | jq -r '
167+
.[] | "- \(.commit.message | split("\n")[0]) (\(.sha[:7]))"
168+
' | head -10)
169+
170+
# 4. Call LLM API to generate tweet
171+
TWEET=$(call_llm_api "$SUMMARY") # abstracted — use Claude/OpenAI/Gemini
172+
173+
# 5. Post tweet
174+
bird tweet post "$TWEET"
175+
# or: python3 post_tweet.py "$TWEET"
176+
177+
# 6. Update state
178+
date -u +%Y-%m-%dT%H:%M:%SZ > "$STATE_FILE"
179+
```
180+
181+
**Python alternative for tweet posting:**
182+
183+
```python
184+
# post_tweet.py
185+
import os, sys, tweepy
186+
187+
client = tweepy.Client(
188+
bearer_token=os.environ["TWITTER_BEARER_TOKEN"],
189+
consumer_key=os.environ["TWITTER_API_KEY"],
190+
consumer_secret=os.environ["TWITTER_API_SECRET"],
191+
access_token=os.environ["TWITTER_ACCESS_TOKEN"],
192+
access_token_secret=os.environ["TWITTER_ACCESS_SECRET"],
193+
)
194+
client.create_tweet(text=sys.argv[1])
195+
```
196+
197+
---
198+
199+
## Decisions Needed (Simon to decide)
200+
201+
Before this can be implemented, the following need human sign-off:
202+
203+
| # | Question | Options |
204+
|---|----------|---------|
205+
| 1 | **Which Twitter account posts?** | Mark's personal + bird CLI / Dedicated `@PlanExeAI` bot account / Egon account with Twitter API creds |
206+
| 2 | **Posting frequency?** | Per-commit / Daily digest / Weekly summary |
207+
| 3 | **Which commits to include?** | All commits / Merge PRs only / Non-trivial commits only (exclude docs, typo, whitespace) |
208+
| 4 | **Content guardrails?** | Max 240 chars (hard Twitter limit) / Banned words list / Require commit URL in every tweet |
209+
| 5 | **Hashtags to always include?** | `#buildinpublic` (definitely) / `#opensource` / `#AI` / `#python` |
210+
| 6 | **LLM for generation?** | Claude Haiku (cheapest Anthropic) / GPT-4o-mini / Gemini Flash / Local (ollama) |
211+
| 7 | **Where does the cron run?** | GitHub Actions (free, native) / Railway cron / VPS / Mark's server |
212+
| 8 | **Error handling** | Silent fail (skip tweet on error) / Alert to Discord / Retry once |
213+
214+
---
215+
216+
## Suggested Starting Configuration
217+
218+
If Simon approves with minimal decisions:
219+
220+
- **Account:** Dedicated `@PlanExeBuilds` or similar (avoids mixing personal/project)
221+
- **Frequency:** Daily digest at 09:00 UTC
222+
- **Commits:** All commits, excluding pure merge commits
223+
- **LLM:** Claude Haiku via Anthropic API (already used in PlanExe)
224+
- **Cron host:** GitHub Actions (`.github/workflows/twitter-digest.yml`) — zero infra cost
225+
- **Guardrails:** 240-char limit enforced by LLM prompt, always include `#buildinpublic`
226+
227+
---
228+
229+
## Open Questions
230+
231+
1. Is anyone opposed to fully automated posting with no human approval? (This is the whole point — if we add approval, it dies.)
232+
2. Should failed LLM calls be silent-failed or reported to a Discord channel?
233+
3. Does Simon want to review the tweet prompt template before it goes live?
234+
4. If the project goes quiet for a week (no commits), should the bot post a "still alive" update, or just stay silent?
235+
5. Should the bot ever reply to comments on its tweets, or post-only?
236+
237+
---
238+
239+
## What This Proposal Does NOT Include
240+
241+
- Working implementation code (that comes after Simon decides on account/frequency)
242+
- Twitter API credential setup instructions (depends on which account is chosen)
243+
- Monitoring/analytics (out of scope for v1)
244+
245+
---
246+
247+
## Next Steps (After Simon's Decisions)
248+
249+
1. Create Twitter account / obtain API credentials
250+
2. Store credentials as GitHub Actions secrets (or Railway env vars)
251+
3. Write `.github/workflows/twitter-digest.yml`
252+
4. Write `scripts/twitter_bot.py` (or shell equivalent)
253+
5. Test with dry-run mode (generate tweet, log to file, don't post)
254+
6. Enable live posting
255+
256+
---
257+
258+
*This is a docs-only proposal. No code changes are included.*

0 commit comments

Comments
 (0)