Skip to content

Merge branch 'dev-tooling' #874

Merge branch 'dev-tooling'

Merge branch 'dev-tooling' #874

Workflow file for this run

# .github/workflows/jekyll.yml
#
# Runs on every push to every branch:
# 1. `bundle exec rubocop` (lint).
# 2. Ruby tests (`load` every _tests/**/test_*.rb).
# 3. Python script tests in _scripts/ (setup-python + `uv run pytest`).
# 4. `_bin/check_strict.rb` (strict Liquid variables).
# 5. `atproto/publish.py validate` — parses every post and book review
# for AT Protocol compatibility; no credentials or network required;
# runs on every branch.
# 6. Build, then four site gates on every branch: the AT-path cross-check
# (validate --site-dir, both directions), HTML structure
# (_bin/check_html_structure.sh), feed XML (_bin/check_feeds.rb), and
# broken links (_bin/check_links.rb). (Only the `test` job's steps run
# on every branch; `build` runs everywhere too but `deploy` only runs
# for main via the `needs:`/`if:` chain.)
#
# Main-only steps in the build job (gated on github.ref == refs/heads/main),
# ordered so PDS records — an external, not-automatically-reversible
# system — are only written after ALL site gates pass:
# 7. `uv run python atproto/publish.py publish` — syncs _posts/ and
# _books/ (and the publication record from _config.yml) to the PDS
# and writes _data/standard_site.json. Requires the BSKY_APP_PASSWORD
# repo secret and a non-empty standard_site.publication_uri (an empty
# value is a hard failure — repo rule 5).
# 8. A second Jekyll build embeds the per-document link tags, then all
# four gates re-run against the artifact that actually deploys.
#
# Test-job concurrency is per-ref and cancels stale runs
# (`test-${{ github.ref }}`); the deploy job's `group: pages` lock is scoped
# to deployment only so it can't starve test runs on other branches.
name: Test and Deploy Jekyll site to Pages
on:
# Runs on pushes to ANY branch
push:
# Allows you to run this workflow manually from the Actions tab.
# skip_publish is the emergency escape hatch: main deploys are otherwise
# hard-coupled to bsky.social uptime because the publish step gates the
# deploy. A manual dispatch with skip_publish=true ships the site without
# touching the PDS (the deploy then carries stale/absent document link
# tags until the next normal main run).
workflow_dispatch:
inputs:
skip_publish:
description: "Skip the AT Protocol publish step (PDS outage escape hatch)"
type: boolean
default: false
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
jobs:
# Test job - Runs on EVERY branch
test:
runs-on: ubuntu-latest
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
cache-version: 0
- name: Run linter
run: bundle exec rubocop
- name: Run tests
run: bundle exec ruby -I _plugins -I _tests -e "require 'test_helper'; Dir.glob('_tests/**/test_*.rb').each { |f| load f }"
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v9.0.0
- name: Lint Python scripts
run: make lint-scripts
- name: Run script tests
run: make test-scripts
# Fast, credential-free subset of the build job's --site-dir
# cross-check (keep the two invocations' flags in sync).
- name: Validate posts for AT Protocol publish
working-directory: _scripts
run: uv run python atproto/publish.py validate --posts-dir ../_posts --books-dir ../_books
- name: Check for strict Liquid errors
run: bundle exec ruby _bin/check_strict.rb
# Build job
build:
runs-on: ubuntu-latest
needs: test
# Serialize builds per ref (no cancellation): overlapping main builds
# would race the publish step's listRecords→createRecord cycle and
# could create duplicate PDS records for the same post path.
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
cache-version: 0
- name: Setup Pages
id: pages
uses: actions/configure-pages@v6
- name: Install uv
uses: astral-sh/setup-uv@v9.0.0
- name: Build with Jekyll
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
# All four site gates must pass BEFORE the publish step below may
# write records to the PDS: records are an external,
# not-automatically-reversible system. The cross-check is a superset
# of the test job's credential-free validate step (keep the two
# invocations' flags in sync).
- name: Cross-check AT record paths against built site
working-directory: _scripts
run: uv run python atproto/publish.py validate --posts-dir ../_posts --books-dir ../_books --site-dir ../_site
- name: Validate HTML structure
run: ./_bin/check_html_structure.sh
- name: Validate feed XML
run: bundle exec ruby _bin/check_feeds.rb
- name: Check for broken links
run: bundle exec ruby _bin/check_links.rb
- name: Publish standard.site records
if: github.ref == 'refs/heads/main' && inputs.skip_publish != true
working-directory: _scripts
run: uv run python atproto/publish.py publish --posts-dir ../_posts --books-dir ../_books --data-out ../_data/standard_site.json
env:
BSKY_HANDLE: alexgude.com
BSKY_APP_PASSWORD: ${{ secrets.BSKY_APP_PASSWORD }}
# The first build ran without _data/standard_site.json (publish had
# not run yet), so main rebuilds to embed the per-document link tags,
# then re-runs every site gate against the artifact that deploys.
- name: Rebuild with document link tags
if: github.ref == 'refs/heads/main'
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Re-check final artifact
if: github.ref == 'refs/heads/main'
run: |
(cd _scripts && uv run python atproto/publish.py validate --posts-dir ../_posts --books-dir ../_books --site-dir ../_site)
./_bin/check_html_structure.sh
bundle exec ruby _bin/check_feeds.rb
bundle exec ruby _bin/check_links.rb
- name: Upload artifact
# Only upload if we are on main (where deployment happens).
# include-hidden-files is required: the action's tar excludes
# dot-prefixed entries by default, which silently dropped
# .well-known/site.standard.publication from the first deploy
# while every _site-level gate stayed green.
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v5
with:
include-hidden-files: true
# Deployment job
deploy:
concurrency:
group: "pages"
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
# This matches the condition above, ensuring we only try to deploy if we uploaded
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
# Artifact-side proof that dot-path files actually deployed; the
# build gates can only see _site/, not what the packaging step kept.
- name: Smoke-test deployed .well-known
run: |
expected=$(grep -A2 '^standard_site:' _config.yml | grep publication_uri | sed 's/.*"\(at:[^"]*\)".*/\1/')
echo "Expecting: $expected"
for i in 1 2 3 4 5; do
got=$(curl -fsS "${{ steps.deployment.outputs.page_url }}.well-known/site.standard.publication" || true)
[ "$got" = "$expected" ] && echo "OK" && exit 0
echo "attempt $i: got '$got'; retrying in 15s (CDN propagation)"
sleep 15
done
echo "ERROR: deployed .well-known does not serve the publication URI"
exit 1