JacHacks Spring 2026: 300+ Builders, 81 Projects, One Weekend of Jac #63
Workflow file for this run
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: Lint new posts | |
| # Blocking PR check: any new file added to docs/blog/posts/ MUST include | |
| # `draft: true` in its frontmatter. This guarantees a PR merge cannot | |
| # accidentally publish a post — the only legitimate way to flip a post live | |
| # is through the scheduling workflows (auto-publisher, /publish-now, | |
| # "Schedule a post" → publish-now). Removing `draft: true` is the dedicated | |
| # job of the github-actions bot, and that path doesn't go through PR review. | |
| # | |
| # Modified existing posts are NOT checked here — auto-publisher commits | |
| # legitimately remove `draft: true`. | |
| # | |
| # Runs on every PR (no path filter) so this check can be marked "required" in | |
| # branch protection without leaving unrelated PRs hanging on a missing status. | |
| # When no posts were added the check passes trivially. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need history to diff against base | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Collect newly added posts | |
| id: added | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -e | |
| ADDED=$(git diff --name-only --diff-filter=A "$BASE_SHA" "$HEAD_SHA" -- 'docs/blog/posts/*.md' || true) | |
| if [ -z "$ADDED" ]; then | |
| echo "No new posts added in this PR — lint passes trivially." | |
| else | |
| echo "Newly added post files:" | |
| echo "$ADDED" | |
| fi | |
| { | |
| echo "files<<EOF" | |
| echo "$ADDED" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Enforce draft on new posts | |
| if: steps.added.outputs.files != '' | |
| env: | |
| FILES: ${{ steps.added.outputs.files }} | |
| run: | | |
| set -e | |
| # FILES is newline-separated; word-splitting on whitespace gives the arg list. | |
| # shellcheck disable=SC2086 | |
| python scripts/schedule_lib.py lint-new-posts $FILES | tee result.json | |
| - name: Guard against un-drafting existing posts | |
| # A PR may legitimately edit an already-live post (no draft: key) — that | |
| # deploys on merge. But a PR must NOT remove `draft: true` from a post that | |
| # is still unpublished: that would publish it on merge, bypassing the | |
| # scheduler. The auto-publisher removes draft: via direct commits to main, | |
| # never through a PR, so this guard never fights it. | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| MODIFIED=$(git diff --name-only --diff-filter=M "$BASE_SHA" "$HEAD_SHA" -- 'docs/blog/posts/*.md' || true) | |
| VIOLATIONS="" | |
| for f in $MODIFIED; do | |
| git show "$BASE_SHA:$f" > /tmp/base_post.md 2>/dev/null || continue | |
| base_draft=$(python scripts/schedule_lib.py check-draft /tmp/base_post.md | python -c "import json,sys;print(json.load(sys.stdin)['draft'])") | |
| head_draft=$(python scripts/schedule_lib.py check-draft "$f" | python -c "import json,sys;print(json.load(sys.stdin)['draft'])") | |
| if [ "$base_draft" = "True" ] && [ "$head_draft" != "True" ]; then | |
| echo "::error file=$f::Removes 'draft: true' from an unpublished post. Keep draft: true and run /schedule after merge — the auto-publisher is the only path to live." | |
| VIOLATIONS="$VIOLATIONS $f" | |
| fi | |
| done | |
| if [ -n "$VIOLATIONS" ]; then | |
| VIOLATIONS="$VIOLATIONS" python -c "import json,os; fs=os.environ['VIOLATIONS'].split(); json.dump({'ok':False,'action':'guard-undraft','failures':[{'path':f,'error':'removes draft: true from an unpublished post'} for f in fs]}, open('result.json','w'), indent=2)" | |
| exit 1 | |
| fi | |
| - name: Post-fail explainer | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let body = '### Lint failed: a post in this PR would publish on merge\n\n'; | |
| body += 'A post is in a state where merging this PR would put it live on blogs.jaseci.org immediately, bypassing the scheduler. This happens when a **new** post is missing `draft: true`, or when a PR **removes** `draft: true` from a post that has not been published yet.\n\n'; | |
| body += 'Publishing is the scheduler\'s job: keep `draft: true`, merge, then `/schedule` the post on its scheduling issue.\n\n'; | |
| body += 'Fix: ensure `draft: true` is present in the frontmatter of the affected file(s):\n\n'; | |
| body += '```yaml\n---\ndate: 2026-XX-XX\nauthors:\n - your_author_id\ncategories:\n - Your Category\nslug: your-slug\ndraft: true # <- this line\n---\n```\n\n'; | |
| body += 'Once an editor merges the PR, they will schedule a publish time using the *Schedule a post* workflow (or `/schedule <ISO datetime>` in this PR\'s comments). The auto-publisher will flip `draft: true` off when that time arrives.\n\n'; | |
| try { | |
| const result = fs.readFileSync('result.json', 'utf8'); | |
| body += '<details><summary>Lint output</summary>\n\n```json\n' + result + '\n```\n\n</details>'; | |
| } catch (e) {} | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| body, | |
| }); |