Post Release to X #58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |