Merge branch 'dev-bluesky' #838
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 for AT Protocol | |
| # compatibility; no credentials or network required; runs on every branch. | |
| # 6. Build, HTML structure validation, feed XML validation, broken-link | |
| # check (only the `test` job's steps run on every branch; `build` and | |
| # `deploy` below only run for main via the `needs:`/`if:` chain). | |
| # | |
| # Main-only steps in the build job (gated on github.ref == refs/heads/main): | |
| # 6. `uv run python atproto/publish.py publish` — syncs _posts/ to PDS | |
| # site.standard.document records and writes _data/standard_site.json | |
| # before Jekyll builds. Requires BSKY_APP_PASSWORD repo secret. | |
| # If this step fails, the build fails and nothing deploys. Exception: | |
| # while _config.yml standard_site.publication_uri is empty (Phase 3 | |
| # of bluesky.md not done), the step is a clean no-op needing no secret. | |
| # | |
| # 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 | |
| workflow_dispatch: | |
| # 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@v7 | |
| - name: Run script tests | |
| working-directory: _scripts | |
| run: uv run pytest | |
| - name: Validate posts for AT Protocol publish | |
| working-directory: _scripts | |
| run: uv run python atproto/publish.py validate --posts-dir ../_posts | |
| - 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 | |
| if: github.ref == 'refs/heads/main' | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Publish standard.site records | |
| if: github.ref == 'refs/heads/main' | |
| working-directory: _scripts | |
| run: uv run python atproto/publish.py publish --posts-dir ../_posts --data-out ../_data/standard_site.json | |
| env: | |
| BSKY_HANDLE: alexgude.com | |
| BSKY_APP_PASSWORD: ${{ secrets.BSKY_APP_PASSWORD }} | |
| - name: Build with Jekyll | |
| run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" | |
| env: | |
| JEKYLL_ENV: production | |
| - name: Validate HTML structure | |
| run: | | |
| echo "Checking for missing DOCTYPE declarations..." | |
| missing_doctype=$(find _site -name '*.html' -exec sh -c 'head -1 "$1" | grep -qv "^<!DOCTYPE" && echo "$1"' _ {} \;) | |
| if [ -n "$missing_doctype" ]; then | |
| echo "ERROR: The following files are missing <!DOCTYPE html>:" | |
| echo "$missing_doctype" | |
| echo "This usually indicates a broken layout chain or corrupted front matter." | |
| exit 1 | |
| fi | |
| echo "Checking for raw front matter in output..." | |
| # Only check for --- at the START of files (actual unprocessed front matter) | |
| # Ignore --- inside code blocks (legitimate content like Jekyll tutorials) | |
| raw_frontmatter=$(find _site -name '*.html' -exec sh -c 'head -1 "$1" | grep -q "^---$" && echo "$1"' _ {} \;) | |
| if [ -n "$raw_frontmatter" ]; then | |
| echo "ERROR: Raw front matter delimiters found at start of HTML files:" | |
| echo "$raw_frontmatter" | |
| exit 1 | |
| fi | |
| echo "All HTML structure checks passed." | |
| - name: Validate feed XML | |
| run: | | |
| bundle exec ruby -rrexml -e ' | |
| feeds = Dir.glob("_site/**/*.xml").select { |f| | |
| content = File.read(f, 512) | |
| content.include?("<feed") || content.include?("<rss") | |
| } | |
| abort "FAILED — no feed XML files found in _site" if feeds.empty? | |
| feeds.each do |path| | |
| print "#{path}: " | |
| raw = File.read(path) | |
| abort "FAILED — starts with raw Liquid/HTML, not XML declaration" unless raw.start_with?("<?xml") | |
| doc = REXML::Document.new(raw) | |
| entries = REXML::XPath.match(doc, "//*[local-name()=\"entry\"]") | |
| abort "FAILED — feed contains no entries" if entries.empty? | |
| puts "OK (#{entries.length} entries)" | |
| end | |
| ' | |
| - name: Check for broken links | |
| run: bundle exec ruby _bin/check_links.rb | |
| - name: Upload artifact | |
| # Only upload if we are on main (where deployment happens) | |
| if: github.ref == 'refs/heads/main' | |
| uses: actions/upload-pages-artifact@v5 | |
| # 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: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 |