177 lines
6.6 KiB
YAML
177 lines
6.6 KiB
YAML
name: Tweet Release
|
|
|
|
on:
|
|
release:
|
|
types: [published]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tweet_text:
|
|
description: "Custom tweet text (include emojis, keep it punchy)"
|
|
required: true
|
|
type: string
|
|
image_url:
|
|
description: "Optional image URL to attach (png/jpg)"
|
|
required: false
|
|
type: string
|
|
|
|
jobs:
|
|
tweet:
|
|
# Skip beta pre-releases on auto trigger
|
|
if: >-
|
|
github.event_name == 'workflow_dispatch' ||
|
|
!github.event.release.prerelease
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Build tweet text
|
|
id: tweet
|
|
shell: bash
|
|
env:
|
|
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
|
RELEASE_URL: ${{ github.event.release.html_url || '' }}
|
|
RELEASE_BODY: ${{ github.event.release.body || '' }}
|
|
MANUAL_TEXT: ${{ inputs.tweet_text || '' }}
|
|
run: |
|
|
if [ -n "$MANUAL_TEXT" ]; then
|
|
# Manual dispatch — use the custom text as-is
|
|
TWEET="$MANUAL_TEXT"
|
|
else
|
|
# Auto trigger — look for <!-- tweet --> block in release body
|
|
# Format in your release notes:
|
|
# <!-- tweet -->
|
|
# ZeroClaw v0.2.0 🐾
|
|
#
|
|
# 🚀 feature one
|
|
# 🔧 feature two
|
|
#
|
|
# the claw strikes
|
|
# <!-- /tweet -->
|
|
TWEET_BLOCK=$(echo "$RELEASE_BODY" | sed -n '/<!-- tweet -->/,/<!-- \/tweet -->/p' | sed '1d;$d' || true)
|
|
|
|
if [ -n "$TWEET_BLOCK" ]; then
|
|
TWEET="$TWEET_BLOCK"
|
|
else
|
|
# Fallback: auto-generate a simple announcement
|
|
TWEET=$(printf "ZeroClaw %s 🐾\n\nFull release notes 👇\n%s" "$RELEASE_TAG" "$RELEASE_URL")
|
|
fi
|
|
fi
|
|
|
|
# Append release URL if not already present and we have one
|
|
if [ -n "$RELEASE_URL" ] && ! echo "$TWEET" | grep -q "$RELEASE_URL"; then
|
|
TWEET=$(printf "%s\n\n%s" "$TWEET" "$RELEASE_URL")
|
|
fi
|
|
|
|
echo "--- Tweet preview ---"
|
|
echo "$TWEET"
|
|
echo "--- ${#TWEET} chars ---"
|
|
|
|
{
|
|
echo "text<<TWEET_EOF"
|
|
echo "$TWEET"
|
|
echo "TWEET_EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Post to X
|
|
shell: bash
|
|
env:
|
|
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
|
|
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET_KEY }}
|
|
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
|
|
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
|
|
TWEET_TEXT: ${{ steps.tweet.outputs.text }}
|
|
IMAGE_URL: ${{ inputs.image_url || '' }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
pip install requests requests-oauthlib --quiet
|
|
|
|
python3 - <<'PYEOF'
|
|
import os, sys, time
|
|
from requests_oauthlib import OAuth1Session
|
|
|
|
consumer_key = os.environ["TWITTER_CONSUMER_KEY"]
|
|
consumer_secret = os.environ["TWITTER_CONSUMER_SECRET"]
|
|
access_token = os.environ["TWITTER_ACCESS_TOKEN"]
|
|
access_token_secret = os.environ["TWITTER_ACCESS_TOKEN_SECRET"]
|
|
tweet_text = os.environ["TWEET_TEXT"]
|
|
image_url = os.environ.get("IMAGE_URL", "")
|
|
|
|
oauth = OAuth1Session(
|
|
consumer_key,
|
|
client_secret=consumer_secret,
|
|
resource_owner_key=access_token,
|
|
resource_owner_secret=access_token_secret,
|
|
)
|
|
|
|
media_id = None
|
|
|
|
# Upload image if provided
|
|
if image_url:
|
|
import requests
|
|
print(f"Downloading image: {image_url}")
|
|
img_resp = requests.get(image_url, timeout=30)
|
|
img_resp.raise_for_status()
|
|
|
|
content_type = img_resp.headers.get("content-type", "image/png")
|
|
# X media upload (v1.1 chunked INIT/APPEND/FINALIZE)
|
|
init_resp = oauth.post(
|
|
"https://upload.twitter.com/1.1/media/upload.json",
|
|
data={
|
|
"command": "INIT",
|
|
"total_bytes": len(img_resp.content),
|
|
"media_type": content_type,
|
|
},
|
|
)
|
|
if init_resp.status_code != 202:
|
|
print(f"Media INIT failed: {init_resp.status_code} {init_resp.text}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
media_id = init_resp.json()["media_id_string"]
|
|
|
|
append_resp = oauth.post(
|
|
"https://upload.twitter.com/1.1/media/upload.json",
|
|
data={"command": "APPEND", "media_id": media_id, "segment_index": 0},
|
|
files={"media_data": img_resp.content},
|
|
)
|
|
if append_resp.status_code not in (200, 204):
|
|
print(f"Media APPEND failed: {append_resp.status_code} {append_resp.text}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
fin_resp = oauth.post(
|
|
"https://upload.twitter.com/1.1/media/upload.json",
|
|
data={"command": "FINALIZE", "media_id": media_id},
|
|
)
|
|
if fin_resp.status_code not in (200, 201):
|
|
print(f"Media FINALIZE failed: {fin_resp.status_code} {fin_resp.text}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Wait for processing if needed
|
|
state = fin_resp.json().get("processing_info", {}).get("state")
|
|
while state == "pending" or state == "in_progress":
|
|
wait = fin_resp.json().get("processing_info", {}).get("check_after_secs", 2)
|
|
time.sleep(wait)
|
|
status_resp = oauth.get(
|
|
"https://upload.twitter.com/1.1/media/upload.json",
|
|
params={"command": "STATUS", "media_id": media_id},
|
|
)
|
|
state = status_resp.json().get("processing_info", {}).get("state")
|
|
fin_resp = status_resp
|
|
|
|
print(f"Image uploaded: media_id={media_id}")
|
|
|
|
# Post tweet
|
|
payload = {"text": tweet_text}
|
|
if media_id:
|
|
payload["media"] = {"media_ids": [media_id]}
|
|
|
|
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
|
|
|
|
if resp.status_code == 201:
|
|
data = resp.json()
|
|
tweet_id = data["data"]["id"]
|
|
print(f"Tweet posted: https://x.com/zeroclawlabs/status/{tweet_id}")
|
|
else:
|
|
print(f"Failed to post tweet: {resp.status_code}", file=sys.stderr)
|
|
print(resp.text, file=sys.stderr)
|
|
sys.exit(1)
|
|
PYEOF
|