Publish scheduled posts via PR + auto-merge (direct push to main never worked); add live scheduled-publish loop - #152
Merged
Conversation
…sh loop The scheduler flipped published:false→true and pushed directly to main with the default GITHUB_TOKEN. Consumer rulesets (pull_request rule + required checks, no bypass actors) reject that push — proven live on 2026-07-10 (adamdaniel.ai run 29110547568: GH013, 'Changes must be made through a pull request') — and even where it could land, a GITHUB_TOKEN push would not fire deploy-production (the documented token-suppression trap). Net effect: scheduled publishing never worked; zero auto-publish commits exist in consumer history, and schedule-event failures have no PR to go red on. Rework the reusable to ride the platform's own path: commit flips to a cms/posts/scheduled-publish-<run_id> branch (Decap-shaped for the labeller and content guard), open a PR as CMS_E2E_PAT, label cms/draft + cms/ready (+ decap-cms/pending_publish per the label-at-creation convention) so auto-merge-when-ready lands it once required checks pass and the merge fires the deploy. Fail loud when posts are due but the secret is missing (a GITHUB_TOKEN PR can never satisfy required checks); keep at most one scheduled-publish PR in flight. Add the cms-scheduled-publish-loop reusable + caller + real-lane spec proving the whole chain live (including not-before-the-deadline), and a pure-fs lint locking the workflow shape. Budgets derive from the fixture helpers' documented 25-min merge windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7rqKwvKrn92eNE5LgCvBq
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug, proven live today
publish-scheduled-posts.ymlflippedpublished: false → truefor due posts and rangit push origin mainwith the default GITHUB_TOKEN. Consumer rulesets reject that outright. Red-run evidence (adamdaniel.ai, an unadvertised noindex/no-sitemap canary seeded via the normalcms/readyauto-merge path withpublish_date: 17:18Z):This was the first time a post ever came due: there are zero
auto-publishcommits in consumer history, and schedule-event failures have no PR to go red on, so the breakage was invisible. Second latent bug: even where the push could land, a GITHUB_TOKEN push doesn't triggerdeploy-production(the token-suppression trap already documented onauto-merge-when-ready).The fix
The reusable now rides the platform's own machinery (same as the delete-recovery and fixture-PR flows):
cms/posts/scheduled-publish-<run_id>— thecms/prefix keeps it Decap-shaped forlabel-non-decap-prs.ymland the content guard (Guard content PRs: fail validate-content when CMS-managed paths change outside Decap #150).CMS_E2E_PATand labeledcms/draft+cms/ready(+decap-cms/pending_publish, the v0.1.48 label-at-creation convention) →auto-merge-when-readyenables auto-merge as the PAT user → merge fires the deploy like any other content merge.permissions:drops tocontents: read(all writes ride the PAT); the example caller passes the secret and documents the requirement.The test (red first, then live loop)
e2e/cms-scheduled-publish-loop.spec.js(@lane: real, API+HTTP only): seeds an unadvertised scheduled draft viaseedFixtureViaPr, asserts 404 + a pre-deadline dispatch is a no-op, waits out the deadline, dispatches again, finds the scheduled-publish PR by its diff, waits for auto-merge + deploy, asserts the URL serves the run marker, deletes, asserts 404. Deadline window is 40 min because the seed itself has a documented 25-min merge budget — a small window would race it (explained in the header).TEST_TIMEOUT_MS150 min ↔ jobtimeout-minutes165, locked by the new lint..github/workflows/cms-scheduled-publish-loop.yml+ examples caller (cron0 5 * * *— free slot in the daily map — + dispatch): mirrorscms-publish-loop-prod.yml(PROD_PLAYGROUND_MODE gate, shared prod-mutating lane, post-failure comment), recursion gate deliberately dropped (schedule/dispatch-only; constant-true for non-push events — documented, and the lint pins the caller to schedule+dispatch).e2e/publish-scheduled-posts-flow.test.js(pure-fs, 14 tests): no run block may containgit push origin main; PAT wiring, fail-loud shape, stacking-guard gating, branch-prefix lockstep withFIXTURE_BRANCH_PREFIX, budget alignment, caller pin parity.cms-scheduled-post.spec.jsworkflow-wiring test asserted the retired push shape (and a cron that aworkflow_call-only file can never satisfy); updated to the PR-flow contract.Test plan
--project=chromium-light): 876 passed, 77 skipped; only failures are the 3 pre-existingfield-library-ref-render.test.jslocale failures (reproduce identically on unmodified main in this container; CI has a proper locale)Rollout
v0.1.60.v0.1.59 → v0.1.60, update thepublish-scheduled-posts.ymlcaller (addsecrets: CMS_E2E_PAT,permissions: contents: read), add thecms-scheduled-publish-loop.ymlcaller.publish-scheduled-posts→ the standing canary publishes through the new PR flow → verify serve → delete the canary. (I'll drive this.)Note:
sweep-stale-cms-prs.yml's safelist deliberately does not covercms/posts/scheduled-publish-*(closing a genuinely pending publish PR would be wrong); the loop's fail-open cleanup handles branch accumulation on loop-running consumers.🤖 Generated with Claude Code
https://claude.ai/code/session_01D7rqKwvKrn92eNE5LgCvBq
Generated by Claude Code