Low-noise GitHub milestones → social posts.
Live: https://git-tweet.abvx.xyz/
Listed on ABVX Lab: https://lab.abvx.xyz/
What it does · Post policy · Quickstart · Real end-to-end test · Local webhook replay
git-tweet is a small conservative tool that watches public GitHub repositories you explicitly activate and auto-posts meaningful release milestones to social networks (currently: X and Bluesky).
The public landing page lives at git-tweet.abvx.xyz. The repo stays the source of truth for setup, policy, and release workflow details.
Operational note:
- keep the public landing and the operator deployment separate
- set
APP_URLto the operator deployment URL, not the public landing URL - point GitHub webhooks to the operator deployment
/api/webhooks/github
It’s designed for “I’m shipping, I forget to post” workflows:
- no AI
- no commit spam
- predictable rules
- full logging and rerun support
Supported in the current stage:
- Release published
- First public release
- Major version
- Semver tag (only when not covered by a release)
Not supported (intentionally out of scope in this stage):
- commits, PRs, branches, issues, stars, “milestones”, LinkedIn, queues/cron, notifications
A single release should produce one post:
FIRST_PUBLIC_RELEASE>MAJOR_VERSION>RELEASE_PUBLISHEDTAG_ONLYis used only when there is no release covering the same semver- For repositories that already use release publishing, semver tag posts are skipped conservatively to avoid duplicate social posts
Each post is structured for readability:
- What happened (Released / Major release / First public release / Tagged)
- What it is (one-line blurb; always present)
- Link (repository URL; stable social preview is preferred over release-page cards)
- 0–2 hashtags (from repo Topics; conservative normalization + optional fallback)
Example:
Released v0.1.2: git-tweet
Auto-post meaningful GitHub releases to social posts (low-noise).
https://github.com/markoblogo/git-tweet
#opensource #devtools
In this stage:
- repo-specific overrides for key repos (e.g.
git-tweet,AGENTS.md_generator) - fallback to GitHub repository description (if present)
- final fallback:
Project update.
In the current stage, git-tweet always uses the repository URL as the post link target.
Reason:
- GitHub repository links produce more stable social preview cards than release pages
- Branding is more predictable
- The behavior stays conservative and easy to reason about
Included:
- GitHub OAuth connect flow
- X OAuth connect flow (default mode)
- Repository sync from connected GitHub account
- Public/private repository distinction (posting: public only)
- Explicit repository activation/deactivation
- Release/tag ingestion with webhook signature verification
- Optional shortener integration with safe fallback
- Logs/history with lifecycle clarity
- Manual rerun path for failed posts
- Local replay scripts for signed webhooks
Out of scope:
- Multi-user SaaS onboarding, billing, org roles
- LinkedIn, AI, queues/cron, notifications, advanced analytics
npm installcp .env.example .env.localMinimal required:
- DATABASE_URL
- APP_URL (operator deployment base URL)
- GITHUB_WEBHOOK_SECRET
Plus OAuth credentials:
- GITHUB_CLIENT_ID
- GITHUB_CLIENT_SECRET
- X_CLIENT_ID
- X_CLIENT_SECRET (for confidential client apps)
- X_CONNECTION_MODE=oauth
Plus Bluesky manual mode credentials:
- BLUESKY_ENABLED=true
- BLUESKY_HANDLE
- BLUESKY_APP_PASSWORD
- BLUESKY_SERVICE_URL=https://bsky.social
npm run db:generate
npm run db:migrate -- --name initnpm run devOpen:
- /connect/github
- /connect/x
- /connect/bluesky
- /repositories
- /logs
For day-to-day code changes, npm run dev is fine.
For real webhook/release testing, prefer production mode:
npm run serve:e2eWhy:
next devcan occasionally produce unstable hot-reload bundles in this repo during long sessionsserve:e2euses a clean production build and is more reliable for GitHub webhook tests, reruns, and UI verification
If the dev server ever starts serving broken CSS/chunks, use:
npm run dev:clean-
GitHub -> Settings -> Developer settings -> OAuth Apps -> New OAuth App
-
Homepage URL:
- Callback URL:
- Copy Client ID/Secret into
.env.local
-
Create an X app with OAuth 2.0
-
App type:
- Web App / Automated App / Bot (confidential client)
- Callback URL:
- App permissions:
- Read and write
- Scopes requested by the app (via
X_OAUTH_SCOPE):
- tweet.read tweet.write users.read offline.access
- Copy Client ID (+ Client Secret) into
.env.local
Bluesky is currently supported in manual env mode only.
Required env vars:
BLUESKY_ENABLED=trueBLUESKY_HANDLEBLUESKY_APP_PASSWORDBLUESKY_SERVICE_URL=https://bsky.social
Notes:
- Use an app password, not your main Bluesky account password
- There is no Bluesky OAuth UI in this stage
- If Bluesky is disabled or not configured, X posting still proceeds and Bluesky is logged as skipped
- Open /connect/github
- Click Connect GitHub
- Complete OAuth
- Click Sync repositories
- Open /connect/x
- Ensure
X_CONNECTION_MODE=oauth - Click Connect X
- Complete OAuth
- Open /connect/bluesky
- Set the Bluesky env vars in
.env.local - Restart the app
- Verify the status page shows Bluesky as configured
- Open /repositories
- Filter (public, private, active, inactive)
- Activate only public repositories you want to post from
- Newly discovered public repos are synced as inactive by default
- Private repos are shown as unsupported and cannot be activated
git-tweet can be used as a central posting hub for another public repository you own, for example markoblogo/lab.abvx.
Minimal flow:
- Start
git-tweetlocally:
npm run serve:e2e- Expose it publicly with ngrok:
ngrok http 3000- In the target repository, add a GitHub webhook:
- Payload URL:
https://<your-ngrok-url>/api/webhooks/github
- Content type:
application/json
- Secret:
- same value as
GITHUB_WEBHOOK_SECRET
- same value as
- Events:
Releases
- In
git-tweet:
- open
/connect/github - sync repositories
- open
/repositories - activate the target public repo
- Publish a GitHub Release in that target repository.
Expected result:
/logsshows a new event for that repo- separate destination records appear for
XandBLUESKY - posts are published without adding any posting logic to the target repository itself
Important:
- GitHub cannot deliver webhooks to
127.0.0.1, so a tunnel is required for local testing - for reliable automatic posting, move the webhook to a stable operator
APP_URLinstead of a temporary tunnel - the webhook URL must include
/api/webhooks/github - the target repo must be explicitly activated inside
git-tweet - draft releases do not post; only published releases do
Optional cleanup after a pure test release:
gh release delete <tag> --repo <owner/repo> --yes
git push origin :refs/tags/<tag>
git tag -d <tag>Before you publish your first release and let git-tweet post it, spend 2 minutes on repo presentation.
Most "ugly posts" come from missing repo metadata, not from git-tweet.
In your repository sidebar (or Settings -> General), set:
- Description: a short one-liner (used as fallback for the "what it is" line)
- Topics: 6-10 relevant topics (used to generate 0-2 hashtags)
- Website (optional): project page or docs link
Tip: if Topics are empty, hashtags may be empty too.
When posting a GitHub release/repo link, social networks typically use GitHub's Social preview image.
Set it once:
- Repo -> Settings -> Social preview -> upload a 1280x640 image (
assets/og.pngis a good default)
This is what becomes the card thumbnail in X and other networks.
For the best post quality, make releases meaningful:
- Use semver tags (
v0.1.0,v1.0.0, ...) - Add a short release title and a few bullet points in release notes
git-tweet links to the repository URL for a more stable social preview card.
If you update repo description/topics, re-sync in git-tweet:
/connect/github-> Sync repositories
That pulls updated description/topics into the local database and improves post output.
Activate only the repos you want to post from:
/repositories-> activate selected public repos
Newly discovered public repos are synced as inactive by default.
GitHub cannot deliver webhooks to 127.0.0.1 / localhost.
For real GitHub release events you need a publicly reachable URL that forwards to your local dev server.
- Start the app:
npm run serve:e2e- Start ngrok in another terminal:
ngrok http 3000- Add GitHub webhook (for each test repo):
Repo -> Settings -> Webhooks -> Add webhook
-
Payload URL:
- https://.ngrok-free.app/api/webhooks/github
-
Content type:
- application/json
-
Secret:
- same as GITHUB_WEBHOOK_SECRET
-
Events:
- Releases
- Branch or tag creation (optional, for tag tests)
- Publish a release in:
- markoblogo/git-tweet
- markoblogo/AGENTS.md_generator
- Verify:
- Open /logs
- You should see a new event + post record
- You should see separate destination rows for
XandBLUESKY - Status should be POSTED (or FAILED with a clear error)
- If POSTED, check your X timeline and Bluesky profile
cloudflared tunnel --url http://localhost:3000Use the provided https://...trycloudflare.com/api/webhooks/github as payload URL.
Fixtures:
- fixtures/webhooks/release-published.json
- fixtures/webhooks/create-tag.json
Commands:
npm run replay:release
npm run replay:tagThese scripts sign payloads using GITHUB_WEBHOOK_SECRET.
This validates:
- webhook verification
- ingestion
- dedup
- tweet composition
- posting to X and Bluesky
- logs/history
But it does not validate real GitHub delivery (use a tunnel for that).
If a release reaches git-tweet but one destination fails, use the built-in recovery path:
- Fix the underlying connection issue (
/connect/xfor expired X OAuth,/connect/blueskyor env for Bluesky). - Open
/logsand rerun the failed destination. - Confirm the destination row changes from
FAILEDtoPOSTED.
This is useful when webhook delivery worked, but a provider token expired after the event was ingested.
- Private repositories are excluded from posting scope
- Newly synced public repositories stay inactive until explicitly activated
- Duplicate events are logged as SKIPPED_DUPLICATE
- Policy/guardrail skips are logged as SKIPPED_POLICY with a reason
- Shortener failures never block post creation
- Manual rerun exists for failed posts (/logs)
- Each destination is logged independently (
SYSTEM,X,BLUESKY)
- POST /api/webhooks/github
- GET /api/connect/github/start
- GET /api/connect/github/callback
- POST /api/connect/github/sync
- GET /api/connect/x/start
- GET /api/connect/x/callback
- PATCH /api/repositories/:repositoryId/activation
- POST /api/posts/:postId/rerun
- Refresh-token renewal flow for X
- Bluesky OAuth UI (manual env mode is sufficient for now)
- Better error surfacing for sync/connect in UI
- Optional: editable per-repo blurb (instead of code overrides)
- Optional: additional social connectors (kept modular)

