Deploy Jekyll site to Pages #87
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: Check online links (lychee) | |
| uses: lycheeverse/lychee-action@v2 | |
| with: | |
| # --remap matches the fully-resolved file URI (not the raw href), so the pattern | |
| # must include the file:// scheme and --root-dir prefix. The (/|$) tail handles | |
| # both `/twinBASIC-docs/page` and bare `/twinBASIC-docs` — lychee strips trailing | |
| # slashes before remap, so we can't require one in the pattern. | |
| # | |
| # `--fallback-extensions html` mirrors what GitHub Pages does at request time: | |
| # an extensionless URL like `/FAQ` is served as `/FAQ.html`. Without the flag | |
| # lychee would flag every pretty permalink on the site. | |
| # | |
| # Lychee, not the Python checker, handles the online tree here because the | |
| # `--remap` flag isn't implemented by scripts/check_links.py; the offline tree | |
| # below has all baseurl prefixes already stripped by the offlinify plugin and | |
| # so doesn't need it. | |
| args: >- | |
| --offline --include-fragments | |
| --fallback-extensions html | |
| --index-files 'index.html,.' | |
| --remap '^file://${{ github.workspace }}/docs/_site${{ steps.pages.outputs.base_path }}(/|$) file://${{ github.workspace }}/docs/_site/' | |
| --root-dir ${{ github.workspace }}/docs/_site | |
| ./_site | |
| workingDirectory: ./docs | |
| fail: true | |
| - name: Set up Python for offline link check | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.14' | |
| cache: 'pip' | |
| - name: Install Python deps | |
| run: pip install -r requirements.txt | |
| - 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: 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 | |
| # 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: 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://`. | |
| Source commit: ${{ github.sha }}. | |
| files: twinbasic-docs-offline.zip | |
| fail_on_unmatched_files: true | |
| make_latest: 'true' |