Deploy Jekyll site to Pages #89
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: Deploy Jekyll site to Pages | |
| # Starting point: https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml | |
| on: | |
| push: | |
| branches: ["staging"] | |
| # Allows you to run this workflow manually from the Actions tab. | |
| # The manual path additionally cuts a GitHub release with the | |
| # offline-browsable site copy attached as a zip; pushes to | |
| # `staging` only deploy to Pages. | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: 'Release tag (leave blank for auto: docs-YYYY-MM-DD-HHMM in UTC)' | |
| required: false | |
| type: string | |
| # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. | |
| # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. | |
| concurrency: | |
| group: "pages" | |
| cancel-in-progress: false | |
| jobs: | |
| # Build job | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Set up Ruby and install gems | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./docs | |
| cache-version: 0 # Increment this number if you need to re-download cached gems | |
| - name: Setup Pages | |
| id: pages | |
| uses: actions/configure-pages@v6 | |
| - name: Write secondary config if available | |
| run: | | |
| echo "${{ vars.JEKYLL_SITE_CONFIG }}" > _site_config.yml | |
| echo "SITE_CONFIG=_site_config.yml" >> "$GITHUB_ENV" | |
| if: ${{ vars.JEKYLL_SITE_CONFIG != '' }} | |
| working-directory: ./docs | |
| - name: Build with Jekyll | |
| run: bundle exec jekyll build --config _config.yml,$SITE_CONFIG --baseurl "${{ steps.pages.outputs.base_path }}" | |
| working-directory: ./docs | |
| env: | |
| JEKYLL_ENV: production | |
| PAGES_REPO_NWO: "${{ github.repository }}" | |
| - name: Set up Python for link checks | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.14' | |
| cache: 'pip' | |
| - name: Install Python deps | |
| run: pip install -r requirements.txt | |
| - name: Check online links (check_links.py) | |
| # `--fallback-extensions html` mirrors what GitHub Pages does at request time: | |
| # an extensionless URL like `/FAQ` is served as `/FAQ.html`. Without the flag | |
| # every pretty permalink on the site would look broken. | |
| # | |
| # `--base-path` strips the Pages baseurl (e.g. `/twinBASIC-docs`) from absolute | |
| # URLs before resolving against `--root-dir`. Equivalent to the `--remap` regex | |
| # that lychee used in earlier iterations of this step. | |
| run: >- | |
| python scripts/check_links.py | |
| --offline --include-fragments | |
| --fallback-extensions html | |
| --index-files 'index.html,.' | |
| --base-path '${{ steps.pages.outputs.base_path }}' | |
| --root-dir docs/_site | |
| docs/_site | |
| - name: Check offline links (check_links.py) | |
| # Strict check on `_site-offline/`: every link must resolve to an actual file | |
| # under `file://`, with no extension fallback. Catches relative links in | |
| # markdown sources that point at a permalink that doesn't match the rendered | |
| # filename (e.g. `[Foo](Foo/)` when Jekyll wrote `Foo.html`, not | |
| # `Foo/index.html`) -- the kind of breakage the online check above hides | |
| # behind `--fallback-extensions html`. | |
| run: >- | |
| python scripts/check_links.py | |
| --offline --include-fragments | |
| --index-files index.html | |
| --root-dir docs/_site-offline | |
| docs/_site-offline | |
| - name: Check for surviving live-site links in offline tree | |
| # Flags any https://docs.twinbasic.com/<path> reference left in | |
| # _site-offline/ HTML outside <code>/<pre> blocks. After offlinify | |
| # strips the jekyll-seo-tag block, anything surviving is a source | |
| # link that points at the live site instead of using a relative or | |
| # /tB/... permalink that resolves locally. The bare root URL | |
| # (https://docs.twinbasic.com[/]) is exempt -- intentional "go to | |
| # the live site" links are allowed. | |
| run: python scripts/check_offline_live_links.py | |
| - name: Check book links (informational) | |
| # Failures do not block the build. The book still has absolute | |
| # intra-site URLs that the chapter transform has not yet rewritten | |
| # and some fragment anchors that are not yet generated. Tracked here | |
| # for visibility until those are fixed. | |
| continue-on-error: true | |
| run: >- | |
| python scripts/check_links.py | |
| --offline --include-fragments | |
| --root-dir docs/_site-pdf | |
| docs/_site-pdf/book.html | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Install Node.js dependencies and Chromium | |
| run: | | |
| npm ci | |
| sudo npx puppeteer browsers install chrome --install-deps | |
| - name: Render book PDF | |
| run: | | |
| mkdir -p _pdf | |
| node render-book.mjs _site-pdf/book.html -o _pdf/book.pdf --outline-tags h1,h2,h3,h4 --additional-script ../perf/detach-pages.js | |
| working-directory: ./docs | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: ./docs/_site | |
| # The next two steps run only on manual dispatch -- they | |
| # package the offline copy of the site (`docs/_site-offline/`, | |
| # produced by the always-on `also_build_offline: true` flag in | |
| # `_config.yml` via `_plugins/offlinify.rb`) and ship it as a | |
| # workflow artifact for the `release` job to attach to a new | |
| # GitHub release. Pushes to `staging` skip this and only | |
| # deploy to Pages. | |
| - name: Package offline site | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| if [ ! -d ./docs/_site-offline ]; then | |
| echo "::error::./docs/_site-offline not found -- ensure also_build_offline is true in _config.yml (or in the JEKYLL_SITE_CONFIG override)" | |
| exit 1 | |
| fi | |
| (cd ./docs/_site-offline && zip -rq "${{ runner.temp }}/twinbasic-docs-offline.zip" .) | |
| - name: Upload offline-site workflow artifact | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: twinbasic-docs-offline-zip | |
| path: ${{ runner.temp }}/twinbasic-docs-offline.zip | |
| # Workflow-internal hand-off to the release job; the | |
| # release itself carries the long-lived copy of the zip. | |
| retention-days: 7 | |
| - name: Upload book PDF workflow artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: twinbasic-docs-book-pdf | |
| path: ./docs/_pdf/book.pdf | |
| # Deployment job | |
| deploy: | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 | |
| # Release job -- runs only on manual dispatch, after a successful | |
| # Pages deploy. Each manual deploy cuts a new GitHub release whose | |
| # tag tracks a publicly visible documentation snapshot at | |
| # https://docs.twinbasic.com, with the offline-browsable copy of | |
| # the site attached as twinbasic-docs-offline.zip. | |
| release: | |
| if: github.event_name == 'workflow_dispatch' | |
| needs: deploy | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # softprops/action-gh-release needs contents:write to create | |
| # tags and publish releases. The top-level permissions block | |
| # only grants contents:read for the build/deploy path. | |
| contents: write | |
| steps: | |
| - name: Download offline-site workflow artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: twinbasic-docs-offline-zip | |
| - name: Download book PDF workflow artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: twinbasic-docs-book-pdf | |
| - name: Compute release tag and name | |
| id: tag | |
| env: | |
| INPUT_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| if [ -n "$INPUT_TAG" ]; then | |
| TAG="$INPUT_TAG" | |
| NAME="$INPUT_TAG" | |
| else | |
| TAG="docs-$(date -u +'%Y-%m-%d-%H%M')" | |
| NAME="Documentation $(date -u +'%Y-%m-%d %H:%M UTC')" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "name=$NAME" >> "$GITHUB_OUTPUT" | |
| - name: Create release with offline-site zip | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: ${{ steps.tag.outputs.name }} | |
| target_commitish: ${{ github.sha }} | |
| body: | | |
| Snapshot of the documentation deployed to <https://docs.twinbasic.com>. | |
| **Offline copy:** download `twinbasic-docs-offline.zip`, extract anywhere, and open `index.html` in any browser — no server required. URLs, navigation, dark mode, and search all work over `file://`. | |
| **PDF book:** download `book.pdf` for an offline-readable, printable copy of the full reference. | |
| Source commit: ${{ github.sha }}. | |
| files: | | |
| twinbasic-docs-offline.zip | |
| book.pdf | |
| fail_on_unmatched_files: true | |
| make_latest: 'true' |