Skip to content

Content Calendar Planner #321

Content Calendar Planner

Content Calendar Planner #321

name: Content Calendar Planner
# The scheduling half of the content calendar. It generates nothing — it reads
# committed content and (re)writes the per-week ledgers at content/_calendar/,
# then opens (or updates) a single pull request with the refreshed ledgers
# (they are a derived index, but main requires PRs). Runs daily to advance
# "today" and pick up new releases, and on any push that changes content/ so
# merging an X batch or a release pack reflows the calendar. The
# `!content/_calendar/**` path filter stops the merged PR from retriggering it.
#
# The PR uses one long-lived branch (auto/calendar) that is force-updated on
# each run, so there's always at most one open "Refresh content calendar" PR
# reflecting the latest schedule. See scripts/plan-calendar.ts.
on:
schedule:
# 00:45 UTC daily — after the weekly X batch has a chance to merge.
- cron: "45 0 * * *"
push:
branches: [main]
paths:
- "content/**"
- "!content/_calendar/**"
workflow_dispatch:
inputs:
date:
description: "Plan as of this date YYYY-MM-DD (omit for today, UTC)"
required: false
type: string
concurrency:
group: calendar-plan
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
# pin: actions/checkout@v4.3.1
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
- name: Set up PNPM
# pin: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
with:
version: 10.11.0
- name: Use Node.js 22
# pin: actions/setup-node@v4.4.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Ensure PR label exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh label create calendar --color "0e8a16" --description "Automated content-calendar ledger refresh" --force
- name: Plan the calendar and open/update a PR
env:
INPUT_DATE: ${{ inputs.date }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
DATE="${INPUT_DATE:-$(date -u +%F)}"
BRANCH="auto/calendar"
echo "Planning as of: $DATE"
# Build the branch fresh off the current main so the PR is a single
# clean commit of the latest ledger state.
git fetch origin main
git checkout -B "$BRANCH" origin/main
pnpm plan-calendar --date "$DATE" --commit
# Validate every written ledger before publishing. A corrupt ledger
# should fail the run, not open a PR.
if [ -d content/_calendar ]; then
for f in content/_calendar/*.json; do
[ -e "$f" ] || continue
week=$(basename "$f" .json)
pnpm validate --pack "_calendar/${week}" --root content --quiet
done
fi
git add content/_calendar
if git diff --cached --quiet; then
echo "calendar unchanged since main — nothing to publish"
exit 0
fi
git commit -m "chore: refresh content calendar (${DATE})"
git push --force origin "$BRANCH"
# Open a PR if one isn't already open for this branch; otherwise the
# force-push above already updated the existing PR.
if [ -z "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')" ]; then
gh pr create \
--base main \
--head "$BRANCH" \
--title "Refresh content calendar" \
--body $'Automated refresh of the content-calendar ledgers (`content/_calendar/*.json`) by the planner.\n\nThis is a derived index — review is a formality; merging updates the live `/calendar`. The `auto/calendar` branch is force-updated on each planner run, so this PR always reflects the latest schedule.' \
--label calendar
else
echo "PR already open for $BRANCH; the force-push updated it."
fi