Skip to content

Post Release to X

Post Release to X #58

name: Post Release to X
on:
workflow_run:
workflows: ["Release"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
models: read
jobs:
notify:
runs-on: ubuntu-latest
# Run if: workflow_dispatch (manual) OR workflow_run completed successfully
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Collect recent releases
id: releases
run: |
# Fetch @evolution-sdk releases created in the last hour with real changes
URL=""
FOUND="false"
: > /tmp/release-notes.md
RELEASES=$(gh api /repos/${{ github.repository }}/releases \
--jq '[.[] | select(.tag_name | startswith("@evolution-sdk/"))]')
NOW=$(date +%s)
for i in $(seq 0 10); do
RELEASE=$(echo "$RELEASES" | jq ".[$i]")
[ "$RELEASE" = "null" ] && break
CREATED=$(echo "$RELEASE" | jq -r .created_at)
CREATED_TS=$(date -d "$CREATED" +%s)
AGE=$((NOW - CREATED_TS))
# Only consider releases from the last hour (same batch)
[ $AGE -ge 3600 ] && continue
BODY=$(echo "$RELEASE" | jq -r .body)
TAG=$(echo "$RELEASE" | jq -r .tag_name)
RELEASE_URL=$(echo "$RELEASE" | jq -r .html_url)
# Skip releases that are only dependency bumps
REAL=$(echo "$BODY" \
| grep -A 100 "Patch Changes\|Minor Changes\|Major Changes" \
| tail -n +2 \
| grep -v "Updated dependencies" \
| grep -v "^[[:space:]]*-[[:space:]]*@" \
| grep -v "^[[:space:]]*$" \
| head -1)
if [ -n "$REAL" ]; then
FOUND="true"
echo "### ${TAG}" >> /tmp/release-notes.md
echo "$BODY" >> /tmp/release-notes.md
echo "" >> /tmp/release-notes.md
# Prefer the evolution package URL
if [[ "$TAG" == "@evolution-sdk/evolution@"* ]] || [ -z "$URL" ]; then
URL="$RELEASE_URL"
fi
fi
done
echo "found=$FOUND" >> $GITHUB_OUTPUT
echo "url=$URL" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
if: steps.releases.outputs.found == 'true'
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Generate tweets with AI
if: steps.releases.outputs.found == 'true'
id: ai
run: |
RESULT=$(RELEASE_NOTES="$(cat /tmp/release-notes.md)" \
RELEASE_URL="${{ steps.releases.outputs.url }}" \
GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \
node .github/scripts/generate-release-tweet.mjs)
{
echo 'tweets<<EOF'
echo "$RESULT"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Post thread to X
if: steps.releases.outputs.found == 'true'
run: |
npm install twitter-api-v2@1
node -e "
const { TwitterApi } = require('twitter-api-v2');
const client = new TwitterApi({
appKey: process.env.TWITTER_API_KEY,
appSecret: process.env.TWITTER_API_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN,
accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
});
(async () => {
const payload = JSON.parse(process.env.TWEETS_JSON);
const tweets = payload.tweets;
if (!tweets || tweets.length === 0) {
console.log('No tweets to post (release was dependency-only).');
return;
}
// Post first tweet
let lastTweet = await client.v2.tweet({ text: tweets[0] });
console.log('Posted tweet 1/' + tweets.length + ': ' + tweets[0]);
// Post remaining tweets as replies (thread)
for (let i = 1; i < tweets.length; i++) {
lastTweet = await client.v2.tweet({
text: tweets[i],
reply: { in_reply_to_tweet_id: lastTweet.data.id }
});
console.log('Posted tweet ' + (i + 1) + '/' + tweets.length + ': ' + tweets[i]);
}
console.log('Thread posted successfully.');
})()
.then(() => process.exit(0))
.catch(err => {
console.error('Twitter API error:', err);
process.exit(1);
});
"
env:
TWEETS_JSON: ${{ steps.ai.outputs.tweets }}
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
TWITTER_API_SECRET: ${{ secrets.TWITTER_API_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}