Merge branch 'dev-tooling' #874
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
| # .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 |