From cde201509ecef3f3791a12f5f4341ca955dbda48 Mon Sep 17 00:00:00 2001 From: kipavy Date: Wed, 7 Oct 2026 19:56:08 +0200 Subject: [PATCH] fix(social): retry the schedule three times a day, post once GitHub ran the 15:00 cron five hours late on Oct 6 and not at all by 18:00 on Oct 7, so the first tweet missed its day. The schedule now fires at 15:17, 18:17 and 21:17 UTC (off the busy :00 minute), and a scheduled run posts nothing when a tweet already went out that UTC day. Manual `post` runs still post right away. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/social.yml | 4 +++- social/README.md | 2 +- social/post.mjs | 9 +++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/social.yml b/.github/workflows/social.yml index b7b6a66..9ea3f7d 100644 --- a/.github/workflows/social.yml +++ b/.github/workflows/social.yml @@ -2,7 +2,9 @@ name: social on: schedule: - - cron: "0 15 * * 1-5" + # Three tries a day: GitHub runs cron late or not at all under load, and :00 is the busiest minute. + # post.mjs posts at most one tweet per day from the schedule. + - cron: "17 15,18,21 * * 1-5" workflow_dispatch: inputs: mode: diff --git a/social/README.md b/social/README.md index 34cc0b0..ccee556 100644 --- a/social/README.md +++ b/social/README.md @@ -1,6 +1,6 @@ # Social posting -`queue.json` holds every planned tweet with its date. The `social` workflow runs at 15:00 UTC on weekdays and posts **one** tweet: the oldest due one that is ready. Nothing is ever posted twice; state lives in the "Social posting log" issue. +`queue.json` holds every planned tweet with its date. The `social` workflow runs on weekdays at 15:17 UTC and posts **one** tweet: the oldest due one that is ready. It runs again at 18:17 and 21:17 in case GitHub runs the schedule late or skips it; those runs post nothing if a tweet already went out that day. Nothing is ever posted twice; state lives in the "Social posting log" issue. - **Text-only tweets** post on their date. - **Tweets with a visual** wait for the file at `social/media/.mp4` (or `.png`, `.jpg`, `.gif`, `.webp`, `.mov`). Images post as soon as they are there and the date has come. diff --git a/social/post.mjs b/social/post.mjs index cc9254d..6633ddf 100644 --- a/social/post.mjs +++ b/social/post.mjs @@ -158,7 +158,7 @@ function logBody(rows, state) { const done = state.posted[e.id]; return `| ${e.date} | #${e.id} | ${done ? `[posted](https://x.com/i/status/${done.tweet})` : status} | ${e.text.slice(0, 60).replace(/\|/g, '/')}… |`; }); - return `Updated by the \`social\` workflow at each run. One tweet posts per weekday at 15:00 UTC: the oldest due one that is ready. Media goes in \`social/media/.mp4\` (or .png/.jpg/.gif); videos need the \`${LABEL.approved}\` label on their approval issue. + return `Updated by the \`social\` workflow at each run. One tweet posts per weekday from 15:17 UTC: the oldest due one that is ready. Media goes in \`social/media/.mp4\` (or .png/.jpg/.gif); videos need the \`${LABEL.approved}\` label on their approval issue. | Date | Tweet | Status | Text | | --- | --- | --- | --- | @@ -191,7 +191,12 @@ async function run(write) { rows.push({ e, file, approval, status }); } - const next = rows.find((r) => r.status === 'ready'); + // The schedule fires several times a day because GitHub delays or drops cron runs; only the first one that finds a tweet posts. + // A manual `post` run always posts. + const postedToday = Object.values(state.posted).some((p) => p.at.slice(0, 10) === today); + const scheduled = process.env.GITHUB_EVENT_NAME === 'schedule'; + const next = scheduled && postedToday ? undefined : rows.find((r) => r.status === 'ready'); + if (scheduled && postedToday) console.log('already posted today'); console.log(rows.filter((r) => r.status !== 'posted' && daysUntil(r.e.date) <= LEAD_DAYS).map((r) => `#${r.e.id} ${r.e.date} ${r.status}`).join('\n') || 'nothing due'); console.log(next ? `next: #${next.e.id} "${next.e.text.slice(0, 70)}…"${next.file ? ` + ${next.file.rel}` : ''}` : 'nothing to post');