Skip to content

Publish scheduled posts via PR + auto-merge (direct push to main never worked); add live scheduled-publish loop - #152

Merged
Adam-S-Daniel merged 1 commit into
mainfrom
claude/scheduled-publish-pr-flow
Jul 10, 2026
Merged

Publish scheduled posts via PR + auto-merge (direct push to main never worked); add live scheduled-publish loop#152
Adam-S-Daniel merged 1 commit into
mainfrom
claude/scheduled-publish-pr-flow

Conversation

@Adam-S-Daniel

Copy link
Copy Markdown
Owner

The bug, proven live today

publish-scheduled-posts.yml flipped published: false → true for due posts and ran git push origin main with the default GITHUB_TOKEN. Consumer rulesets reject that outright. Red-run evidence (adamdaniel.ai, an unadvertised noindex/no-sitemap canary seeded via the normal cms/ready auto-merge path with publish_date: 17:18Z):

  • 17:05Z dispatch (pre-deadline): run 29109731788 — success as a no-op, URL 404 ✅ (scheduler correctly does nothing before the deadline)
  • 17:19Z dispatch (post-deadline): run 29110547568failure:
    Publishing 2026-07-10-e2e-scheduled-publish-1783702433.md  (scheduled: 2026-07-10T17:18:00+00:00)
    remote: error: GH013: Repository rule violations found for refs/heads/main.
    remote: - Changes must be made through a pull request.
    remote: - 6 of 6 required status checks are expected.
    ! [remote rejected] main -> main (push declined due to repository rule violations)
    

This was the first time a post ever came due: there are zero auto-publish commits 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 trigger deploy-production (the token-suppression trap already documented on auto-merge-when-ready).

The fix

The reusable now rides the platform's own machinery (same as the delete-recovery and fixture-PR flows):

  1. Script flips due posts (unchanged).
  2. Flips are committed to cms/posts/scheduled-publish-<run_id> — the cms/ prefix keeps it Decap-shaped for label-non-decap-prs.yml and the content guard (Guard content PRs: fail validate-content when CMS-managed paths change outside Decap #150).
  3. A PR is opened as CMS_E2E_PAT and labeled cms/draft + cms/ready (+ decap-cms/pending_publish, the v0.1.48 label-at-creation convention) → auto-merge-when-ready enables auto-merge as the PAT user → merge fires the deploy like any other content merge.
  4. Fail-loud, no degraded mode when posts are due but the secret is missing (a GITHUB_TOKEN PR can never satisfy required checks). Stacking guard: at most one scheduled-publish PR in flight.
  5. permissions: drops to contents: 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 via seedFixtureViaPr, 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_MS 150 min ↔ job timeout-minutes 165, locked by the new lint.
  • .github/workflows/cms-scheduled-publish-loop.yml + examples caller (cron 0 5 * * * — free slot in the daily map — + dispatch): mirrors cms-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 contain git push origin main; PAT wiring, fail-loud shape, stacking-guard gating, branch-prefix lockstep with FIXTURE_BRANCH_PREFIX, budget alignment, caller pin parity.
  • The dormant cms-scheduled-post.spec.js workflow-wiring test asserted the retired push shape (and a cron that a workflow_call-only file can never satisfy); updated to the PR-flow contract.

Test plan

  • Full self-CI pure-fs suite (deny-filtered, --project=chromium-light): 876 passed, 77 skipped; only failures are the 3 pre-existing field-library-ref-render.test.js locale failures (reproduce identically on unmodified main in this container; CI has a proper locale)
  • New lint 14/14; all workflow YAMLs parse (PyYAML + the suite's eemeli-yaml lints)
  • Green run on prod after release + consumer bump (the red-run canary is still on adamdaniel.ai main, unpublished and past-due — it becomes the green-run fixture)

Rollout

  1. Merge → cut v0.1.60.
  2. Consumer bump (adamdaniel.ai): pins v0.1.59 → v0.1.60, update the publish-scheduled-posts.yml caller (add secrets: CMS_E2E_PAT, permissions: contents: read), add the cms-scheduled-publish-loop.yml caller.
  3. Dispatch publish-scheduled-posts → the standing canary publishes through the new PR flow → verify serve → delete the canary. (I'll drive this.)
  4. jodidaniel.com follows via the usual consumer checklist (cms-platform#147).

Note: sweep-stale-cms-prs.yml's safelist deliberately does not cover cms/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

…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
@Adam-S-Daniel
Adam-S-Daniel merged commit f488b0c into main Jul 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants