Auto-publish scheduled posts #972
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: Auto-publish scheduled posts | |
| # Runs hourly. Reads docs/blog/.schedule.yml, finds entries whose publish_at | |
| # has passed, removes `draft: true` from each target post, marks the entry | |
| # as published, and commits. The resulting commit to `main` triggers the | |
| # normal deploy workflow. | |
| on: | |
| schedule: | |
| - cron: "5 * * * *" # five-past every hour, UTC | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| actions: write # to trigger deploy.yml via workflow_dispatch | |
| concurrency: | |
| group: schedule-mutation | |
| cancel-in-progress: false | |
| jobs: | |
| publish-due: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Publish due entries | |
| id: run | |
| run: | | |
| set -e | |
| python scripts/schedule_lib.py publish-due --actor "auto-publisher" | tee result.json | |
| published_count=$(python -c "import json; d=json.load(open('result.json')); print(len(d.get('published', [])))") | |
| echo "count=$published_count" >> "$GITHUB_OUTPUT" | |
| - name: Commit and push | |
| if: steps.run.outputs.count != '0' | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -e | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| slugs=$(python -c "import json; d=json.load(open('result.json')); print(', '.join(p['slug'] for p in d['published']))") | |
| git add docs/blog/ | |
| git commit -m "auto-publish: $slugs" \ | |
| -m "Triggered by hourly scheduler." \ | |
| -m "Run: $RUN_URL" | |
| git push origin main | |
| - name: Trigger deploy | |
| # A push made with the default GITHUB_TOKEN does NOT fire deploy.yml's | |
| # `on: push`. workflow_dispatch is exempt from that rule, so kick it | |
| # explicitly to ship the just-published posts. | |
| if: steps.run.outputs.count != '0' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: gh workflow run deploy.yml --ref main | |
| - name: Close scheduling issues | |
| id: closed | |
| if: steps.run.outputs.count != '0' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const result = JSON.parse(fs.readFileSync('result.json', 'utf8')); | |
| // The Jac site serves posts at a stable slug-only URL, so the live | |
| // URL is the same link the tracker/docs issues already advertised. | |
| const published = (result.published || []).map(p => ({ | |
| slug: p.slug, | |
| url: `https://blogs.jaseci.org/blog/posts/${p.slug}`, | |
| })); | |
| core.setOutput('published', JSON.stringify(published)); | |
| const open = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| state: 'open', labels: 'blog-schedule', per_page: 100, | |
| }); | |
| for (const pub of published) { | |
| const found = open.find(i => (i.body || '').includes(`slug=${pub.slug} `)); | |
| if (!found) continue; | |
| const msg = pub.url ? `🎉 Published — now live at ${pub.url}` : '🎉 Published.'; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: found.number, body: msg, | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: found.number, state: 'closed', | |
| }); | |
| } | |
| - name: Mint cross-repo token | |
| id: app-token | |
| if: steps.run.outputs.count != '0' && vars.ISSUE_BOT_APP_ID != '' | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ vars.ISSUE_BOT_APP_ID }} | |
| private-key: ${{ secrets.ISSUE_BOT_PRIVATE_KEY }} | |
| owner: ${{ vars.DOCS_REPO_OWNER || 'jaseci-labs' }} | |
| repositories: ${{ vars.DOCS_REPO_NAME || 'jaseci' }} | |
| - name: Comment live URL on docs issues | |
| if: steps.run.outputs.count != '0' && vars.ISSUE_BOT_APP_ID != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| DOCS_OWNER: ${{ vars.DOCS_REPO_OWNER || 'jaseci-labs' }} | |
| DOCS_NAME: ${{ vars.DOCS_REPO_NAME || 'jaseci' }} | |
| PUBLISHED: ${{ steps.closed.outputs.published }} | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const owner = process.env.DOCS_OWNER, repo = process.env.DOCS_NAME; | |
| const published = JSON.parse(process.env.PUBLISHED || '[]'); | |
| const open = await github.paginate(github.rest.issues.listForRepo, { | |
| owner, repo, state: 'open', labels: 'blog-docs-sync', per_page: 100, | |
| }); | |
| for (const pub of published) { | |
| if (!pub.url) continue; | |
| const found = open.find(i => (i.body || '').includes(`slug=${pub.slug} `)); | |
| if (!found) continue; | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: found.number, | |
| body: `The post is now live: ${pub.url}\n\nThis is the final, dated URL — please point any \`coming-soon\` links here.`, | |
| }); | |
| } | |
| - name: Job summary | |
| if: always() | |
| run: | | |
| { | |
| echo "### Auto-publish run" | |
| echo | |
| echo '```json' | |
| cat result.json 2>/dev/null || echo "{}" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" |