Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4b2a21c61 | |||
| d6170ab49b | |||
| 399c896c3b |
@@ -0,0 +1,176 @@
|
||||
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
|
||||
@@ -84,7 +84,7 @@ Built by students and members of the Harvard, MIT, and Sundai.Club communities.
|
||||
|
||||
<p align="center"><code>Trait-driven architecture · secure-by-default runtime · provider/channel/tool swappable · pluggable everything</code></p>
|
||||
|
||||
### 🚀 What's New in v0.1.9a (March 2026)
|
||||
### 🚀 What's New in v0.1.9b (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
@@ -1142,7 +1142,7 @@ A heartfelt thank you to the communities and institutions that inspire and fuel
|
||||
|
||||
We're building in the open because the best ideas come from everywhere. If you're reading this, you're part of it. Welcome. 🦀❤️
|
||||
|
||||
### 🌟 Recent Contributors (v0.1.9a)
|
||||
### 🌟 Recent Contributors (v0.1.9b)
|
||||
|
||||
Special recognition to the contributors who shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user