Skip to content

Commit ace9f05

Browse files
committed
Add Instagram reel auto-posting pipeline
Weekly GitHub Actions workflow that generates the next edition's reel, commits it for GitHub Pages to serve, and publishes to Instagram via the Meta Graph API Content Publishing flow (create container → poll → publish). - tools/post_reel.py: Graph API publisher, deterministic suit rotation, polls the public URL until live, dry-run mode - tools/captions.json: per-edition captions + shared footer (link/hashtags) - tools/refresh_token.py + refresh-token.yml: monthly long-lived token refresh so the poster stays hands-off - tools/REELS_AUTOMATION.md: one-time Meta app + secrets setup guide Tokens/secrets live only in GitHub Actions secrets, never in the repo. https://claude.ai/code/session_01VhWRYXbuqapZ9YkvksroaS
1 parent 30f05f1 commit ace9f05

6 files changed

Lines changed: 449 additions & 0 deletions

File tree

.github/workflows/post-reel.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Post weekly reel to Instagram
2+
3+
# Generates the week's reel, commits it so GitHub Pages serves it at a public
4+
# URL, then publishes it to Instagram via the Meta Graph API.
5+
#
6+
# Required repo secrets (Settings → Secrets and variables → Actions):
7+
# IG_USER_ID Instagram Business account id
8+
# IG_ACCESS_TOKEN long-lived access token
9+
# See tools/REELS_AUTOMATION.md for the one-time setup.
10+
11+
on:
12+
schedule:
13+
- cron: "0 16 * * 3" # every Wednesday 16:00 UTC
14+
workflow_dispatch:
15+
inputs:
16+
key:
17+
description: "Edition key to post (blank = auto rotation)"
18+
required: false
19+
default: ""
20+
color:
21+
description: "Ink color"
22+
required: false
23+
default: "red"
24+
dry_run:
25+
description: "Build + print only, do not publish"
26+
type: boolean
27+
required: false
28+
default: false
29+
30+
permissions:
31+
contents: write # to commit the generated mp4
32+
33+
concurrency:
34+
group: post-reel
35+
cancel-in-progress: false
36+
37+
jobs:
38+
post:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- uses: actions/setup-python@v5
44+
with:
45+
python-version: "3.11"
46+
47+
- name: Install fonts and Python deps
48+
run: |
49+
sudo apt-get update
50+
sudo apt-get install -y fonts-ipafont-gothic fonts-liberation
51+
python -m pip install --upgrade pip
52+
pip install pillow numpy imageio-ffmpeg
53+
54+
- name: Resolve edition key
55+
id: pick
56+
run: |
57+
KEY="${{ github.event.inputs.key }}"
58+
if [ -z "$KEY" ]; then
59+
KEY=$(python - <<'PY'
60+
import json, time
61+
src=open("data/editions.js").read(); src=src[src.index("{"):src.rindex("}")+1]
62+
order=json.loads(src).get("plotterOrder")
63+
print(order[int(time.time()//(7*86400))%len(order)])
64+
PY
65+
)
66+
fi
67+
COLOR="${{ github.event.inputs.color }}"; COLOR="${COLOR:-red}"
68+
echo "key=$KEY" >> "$GITHUB_OUTPUT"
69+
echo "color=$COLOR" >> "$GITHUB_OUTPUT"
70+
echo "Posting: $KEY ($COLOR)"
71+
72+
- name: Generate reel
73+
run: |
74+
mkdir -p reels
75+
python - <<PY
76+
import sys; sys.path.insert(0, "tools")
77+
import gen_reel
78+
gen_reel.render("${{ steps.pick.outputs.key }}", "${{ steps.pick.outputs.color }}",
79+
dur=15, fps=30,
80+
out="reels/${{ steps.pick.outputs.key }}-${{ steps.pick.outputs.color }}-reel.mp4")
81+
PY
82+
83+
- name: Commit reel so Pages can serve it
84+
run: |
85+
git config user.name "plotflow-bot"
86+
git config user.email "bot@plotflow.io"
87+
git add reels/*.mp4
88+
if git diff --staged --quiet; then
89+
echo "No change to reel file."
90+
else
91+
git commit -m "Add reel for ${{ steps.pick.outputs.key }} [skip ci]"
92+
git push origin HEAD:${{ github.ref_name }}
93+
fi
94+
95+
- name: Publish to Instagram
96+
env:
97+
IG_USER_ID: ${{ secrets.IG_USER_ID }}
98+
IG_ACCESS_TOKEN: ${{ secrets.IG_ACCESS_TOKEN }}
99+
run: |
100+
ARGS="--key ${{ steps.pick.outputs.key }} --color ${{ steps.pick.outputs.color }}"
101+
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then ARGS="$ARGS --dry-run"; fi
102+
python tools/post_reel.py $ARGS
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Refresh Instagram token
2+
3+
# Long-lived Meta tokens expire after 60 days. This refreshes monthly and
4+
# stores the new token back as the IG_ACCESS_TOKEN secret using the gh CLI
5+
# (which handles the libsodium encryption for you).
6+
#
7+
# Required secrets:
8+
# FB_APP_ID, FB_APP_SECRET from your Meta app
9+
# IG_ACCESS_TOKEN current long-lived token (this workflow updates it)
10+
# GH_PAT fine-grained PAT with "Secrets: write" on this repo
11+
# See tools/REELS_AUTOMATION.md.
12+
13+
on:
14+
schedule:
15+
- cron: "0 5 1 * *" # 1st of each month, 05:00 UTC
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
refresh:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: actions/setup-python@v5
27+
with:
28+
python-version: "3.11"
29+
30+
- name: Exchange for a fresh token
31+
id: refresh
32+
env:
33+
FB_APP_ID: ${{ secrets.FB_APP_ID }}
34+
FB_APP_SECRET: ${{ secrets.FB_APP_SECRET }}
35+
IG_ACCESS_TOKEN: ${{ secrets.IG_ACCESS_TOKEN }}
36+
run: python tools/refresh_token.py
37+
38+
- name: Store the new token as a secret
39+
env:
40+
GH_TOKEN: ${{ secrets.GH_PAT }}
41+
run: |
42+
gh secret set IG_ACCESS_TOKEN \
43+
--repo "${{ github.repository }}" \
44+
--body "${{ steps.refresh.outputs.token }}"
45+
echo "IG_ACCESS_TOKEN updated."

tools/REELS_AUTOMATION.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Instagram Reel automation
2+
3+
Auto-generates and posts a PLOTFLOW reel to Instagram on a schedule, with no
4+
manual steps after the one-time setup below.
5+
6+
## How it works
7+
8+
```
9+
GitHub Actions (weekly cron)
10+
→ tools/gen_reel.py renders reels/<key>-red-reel.mp4 (1080×1920 H.264)
11+
→ commit to main GitHub Pages serves it at plotflow.io/reels/<key>-red-reel.mp4
12+
→ tools/post_reel.py Meta Graph API: create container → poll → publish
13+
caption pulled from tools/captions.json
14+
```
15+
16+
Suit rotation is deterministic: `week_since_epoch % 6`, so each week posts the
17+
next edition. Trigger manually anytime (and pick a specific suit) from the
18+
Actions tab → **Post weekly reel to Instagram***Run workflow*.
19+
20+
## One-time setup
21+
22+
### 1. Account prerequisites
23+
- Convert @plotflow to an Instagram **Business** or **Creator** account
24+
(Settings → Account type).
25+
- Link it to a **Facebook Page** (Instagram Settings → linked accounts).
26+
27+
### 2. Create a Meta app
28+
1. Go to https://developers.facebook.com/apps**Create App** → type *Business*.
29+
2. Add the **Instagram** product (Instagram Graph API).
30+
3. Note the **App ID** and **App Secret** (Settings → Basic).
31+
32+
### 3. Get the IDs and a long-lived token
33+
Use the **Graph API Explorer** (Tools menu) with your app selected:
34+
1. Generate a user token with these permissions:
35+
`instagram_basic`, `instagram_content_publish`, `pages_show_list`,
36+
`business_management`.
37+
2. Find your **Instagram Business account id**:
38+
`GET /me/accounts` → get the Page id → `GET /{page-id}?fields=instagram_business_account`.
39+
The returned `instagram_business_account.id` is your **IG_USER_ID**.
40+
3. Exchange the short-lived token for a **long-lived** one (≈60 days):
41+
```
42+
GET https://graph.facebook.com/v21.0/oauth/access_token
43+
?grant_type=fb_exchange_token
44+
&client_id={APP_ID}
45+
&client_secret={APP_SECRET}
46+
&fb_exchange_token={SHORT_LIVED_TOKEN}
47+
```
48+
The `access_token` in the response is your **IG_ACCESS_TOKEN**.
49+
50+
### 4. App Review
51+
To publish to the public, submit `instagram_content_publish` for **App Review**.
52+
While the app is in *Development* mode you can only publish to accounts that
53+
have a role on the app — add @plotflow's user as a *Tester* (App Roles) to test
54+
end-to-end before review is approved.
55+
56+
### 5. Add repo secrets
57+
Repo → **Settings → Secrets and variables → Actions → New repository secret**:
58+
59+
| Secret | Value | Used by |
60+
|---|---|---|
61+
| `IG_USER_ID` | Instagram Business account id | poster |
62+
| `IG_ACCESS_TOKEN` | long-lived token | poster + refresh |
63+
| `FB_APP_ID` | Meta app id | token refresh |
64+
| `FB_APP_SECRET` | Meta app secret | token refresh |
65+
| `GH_PAT` | fine-grained PAT, *Secrets: write* on this repo | token refresh |
66+
67+
`GH_PAT` is only needed so the monthly refresh can write the new token back.
68+
Create it at GitHub → Settings → Developer settings → Fine-grained tokens,
69+
scoped to this repo with **Secrets: Read and write**.
70+
71+
### 6. Token refresh (keeps it hands-off)
72+
`.github/workflows/refresh-token.yml` runs on the 1st of each month, exchanges
73+
the current token for a fresh 60-day one, and stores it back. Without this the
74+
poster will start failing ~60 days after setup.
75+
76+
## Testing without posting
77+
```bash
78+
# Local dry run — builds the caption + URL, no API call:
79+
python3 tools/post_reel.py --key zaku --dry-run
80+
```
81+
Or in Actions: *Run workflow* with **dry_run = true**.
82+
83+
## Editing captions
84+
Edit `tools/captions.json`. Each edition has its own body; `_footer` (link +
85+
hashtags) is appended to all of them. No code changes needed.
86+
87+
## Notes & limits
88+
- Instagram allows **50 API-published posts per 24h** — far above our cadence.
89+
- The video must be reachable at its public URL before publishing; the poster
90+
polls GitHub Pages until the file is live (Pages deploys take ~1 min).
91+
- Secrets live only in GitHub Actions — never commit tokens to the repo.

tools/captions.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"_comment": "Instagram captions for the auto-poster. One entry per edition key. Edit freely. The poster appends the standard footer (link + hashtags) unless you set 'raw': true.",
3+
"_footer": "\n\n— Plotted to order, signed & numbered.\nShop the edition → plotflow.io\n\n#penplotter #axidraw #generativeart #plotterart #gundam #mobilesuit #lineart #マシンドロー #onelinedrawing #pendrawing #machinedrawn #gunpla #universalcentury #plottertwitter #penart",
4+
"captions": {
5+
"zaku": "MS-06 Zaku II — drawn by machine, one line at a time.\n\nThe workhorse of Zeon, rebuilt as a single continuous path and traced in ink by an AxiDraw plotter. No printing. Every impression is drawn.\n\nED. 09/25 · 11×14″ · $45",
6+
"dom": "MS-09 Dom — hovering in, one stroke at a time.\n\nA ground-assault suit riding hover thrusters, redrawn as a single plotted line on Strathmore Bristol. Pen to paper, no ink-jet in sight.\n\nED. 12/25 · 11×14″ · $45",
7+
"guncannon": "RX-77 Guncannon — firepower in fineliner.\n\nThe Federation's shoulder-cannon support unit, traced line by line in pigment ink by a pen plotter. Heavier armor, heavier linework.\n\nED. 18/25 · 11×14″ · $45",
8+
"bigzam": "MA-08 Big Zam — a colossus, drawn by hand of machine.\n\nThe mobile armor meant to hold off a fleet, rebuilt as one continuous path and plotted in ink. Watch the I-field take shape, stroke by stroke.\n\nED. 05/25 · 11×14″ · $45",
9+
"zgok": "MSM-07 Z'Gok — surfacing, one line at a time.\n\nZeon's amphibious raider, redrawn as a single plotted path. Clawed, rounded, and traced in ink on archival paper by an AxiDraw.\n\nED. 22/25 · 11×14″ · $45",
10+
"gp02": "RX-78GP02 GP-02A — the heavy hitter, plotted in pen.\n\nThe Gundam Development Project's warhead carrier, rebuilt as one continuous line and drawn by machine. Massive shield, single stroke.\n\nED. 15/25 · 11×14″ · $45"
11+
}
12+
}

0 commit comments

Comments
 (0)