Repository navigation
Content Calendar Planner #321
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
| 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 |