Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 147 additions & 41 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
name: docs

# Every documentation version is a separate Hugo build, published under
# https://go-widgets.github.io/docs/<version>/ on the gh-pages branch, where
# <version> is the documentation's own minor version
# (params.docs.version in hugo.yaml: v0.2.0 -> 0.2). main is that
# version's source: each push to main replaces /docs/<version>/, and
# /docs/latest/ follows the newest. Pull requests build and check, and publish
# nothing.

on:
push:
branches: [main]
Expand All @@ -12,73 +20,171 @@ on:
permissions:
contents: read

env:
HUGO_VERSION: "0.167.0"
# sha256 of hugo_${HUGO_VERSION}_linux-amd64.tar.gz, from the release's checksums.txt.
HUGO_SHA256: "4d84519b9f619e6d4c3fb45a50157abeabeb724f859c60605f44c23def6e1169"
DOCS_URL: "https://go-widgets.github.io/docs"

jobs:
# Build on every pull request as well as on main, so a change to the docs
# or to the toolchain in requirements.txt has to prove it still builds
# before it lands. Without this the workflow ran only on main, which meant
# a dependency bump had no signal at all.
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
# themes/hextra (the tannevaled/hextra fork, pinned to a release tag)
# and branding (go-widgets/brand) are submodules.
submodules: recursive
# The whole history: each page's creation and modification come from git.
fetch-depth: 0

- name: Install MkDocs Material + mike
- name: Install Hugo ${{ env.HUGO_VERSION }}
run: |
pip install --upgrade pip
pip install -r requirements.txt
curl -fsSL -o /tmp/hugo.tar.gz \
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-amd64.tar.gz"
echo "${HUGO_SHA256} /tmp/hugo.tar.gz" | sha256sum -c -
sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo
hugo version

# The version label is the documentation's minor version, read from hugo.yaml,
# and it is the last segment of baseURL: the version selector reads it there.
- name: Version
id: version
run: |
docs=$(hugo config --format json | jq -r '.params.docs.version')
version=$(printf '%s\n' "${docs}" | sed -nE 's/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/\1.\2/p')
if [ -z "${version}" ]; then
echo "::error::params.docs.version in hugo.yaml is \"${docs}\", not vMAJOR.MINOR.PATCH"
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "docs ${docs}: documentation version ${version}"

# Each page's creation (first commit, followed across renames) and its
# author, for the page history at its foot (params.page.history).
- name: Page history
run: |
mkdir -p data
sh themes/hextra/scripts/page-history.sh > data/pagehistory.json

# --panicOnWarning: a deprecated setting or a template warning fails the
# build rather than scrolling by. A broken {{< relref >}} is an error.
- name: Build
env:
HUGO_ENVIRONMENT: production
run: >-
hugo --gc --minify --panicOnWarning --printPathWarnings
--baseURL "${DOCS_URL}/${{ steps.version.outputs.version }}/"

# Internal links and anchors, offline, with the site laid out under
# /docs/<version>/ as it is served.
- name: Lay out the site for the link check
run: |
mkdir -p _check/docs
cp -R public "_check/docs/${{ steps.version.outputs.version }}"

- name: Check links
uses: lycheeverse/lychee-action@v2
with:
args: >-
--offline --no-progress --include-fragments
--index-files index.html
--root-dir ${{ github.workspace }}/_check
'_check/docs/**/*.html'
fail: true

# --strict turns a broken link or an unknown config key into a failure
# rather than a warning nobody reads.
- name: Build the site (strict)
run: mkdocs build --strict
- uses: actions/upload-artifact@v7
with:
name: site
path: public
retention-days: 7

# The offline check above proves the site's own links and anchors. An
# https:// URL is text to it: this job fetches each one the pages name.
links:
name: every external link resolves
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check
run: |
set -uo pipefail
# The character class matters: an extractor stopping only at ) and "
# carries a closing > or < into the request.
urls=$(grep -rhoE 'https?://[^)">< [:space:]]+' content/ README.md | sed -E 's/[.,`]+$//' | sort -u)
test -n "$urls" || { echo "::error::no links found; are the pages still there?"; exit 1; }
echo "checking $(printf '%s\n' "$urls" | wc -l) distinct URLs"
fail=0
while read -r u; do
case "$u" in
# Renders whatever it is asked for; proves nothing.
*img.shields.io*) continue ;;
# Documented placeholders, not hosts.
*example.com*|*example.org*|*example.net*|*localhost*|*127.0.0.1*|*.invalid*) continue ;;
esac
# Three attempts: one is a flake detector rather than a link
# checker, and a lane that fails at random gets ignored.
code=000
for _ in 1 2 3; do
code=$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 25 "$u" 2>/dev/null) || true
code=${code:-000}
case "$code" in 2*|3*) break ;; esac
sleep 3
done
case "$code" in
2*|3*) printf ' ok %s %s\n' "$code" "$u" ;;
*) printf ' FAIL %s %s\n' "$code" "$u"; fail=1 ;;
esac
done <<< "$urls"
test "$fail" -eq 0 || { echo "::error::at least one link does not resolve"; exit 1; }

deploy:
# Only main publishes. A pull request stops after build above: it has no
# business pushing to gh-pages.
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
# Never two deploys at once: mike pushes gh-pages and the second is
# rejected outright. cancel-in-progress stays false -- killing a publish
# half way through is worse than making it wait.
# Never two deploys at once: both would push gh-pages and the second would
# be rejected. cancel-in-progress stays false -- killing a publish half way
# through is worse than making it wait.
concurrency:
group: gh-pages-deploy
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v7
# The publishing script, from the commit being published.
- uses: actions/checkout@v7
with:
fetch-depth: 0
path: source

- name: Set up Python
uses: actions/setup-python@v7
# The published site. Only the version being built is replaced; every
# other version (the MkDocs-built 0.1) stays as it is.
- uses: actions/checkout@v7
with:
python-version: "3.14"
ref: gh-pages
path: gh-pages

- name: Install MkDocs Material + mike
run: |
pip install --upgrade pip
pip install -r requirements.txt
- uses: actions/download-artifact@v7
with:
name: site
path: site

- name: Configure git for mike
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Publish ${{ needs.build.outputs.version }}
run: source/scripts/publish-version.sh "${{ needs.build.outputs.version }}" site gh-pages

# mike publishes versioned docs to the gh-pages branch; GitHub Pages serves
# that branch at https://go-compressions.github.io/docs/. The version label
# tracks main as "0.1" aliased "latest"; tag-driven versions can be added
# later. /docs/ redirects to the default ("latest").
- name: Deploy versioned docs
- name: Push gh-pages
working-directory: gh-pages
run: |
git fetch origin gh-pages --depth=1 || true
mike deploy --push --update-aliases 0.1 latest
mike set-default --push latest
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "nothing changed"
exit 0
fi
git commit -q -m "Deployed ${GITHUB_SHA::7} to ${{ needs.build.outputs.version }} with Hugo ${HUGO_VERSION}"
git push origin HEAD:gh-pages
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/site/
.venv/
__pycache__/
/public/
/resources/
/_check/
.hugo_build.lock
.DS_Store

# Written by themes/hextra/scripts/page-history.sh before each build (CI and local).
/data/pagehistory.json
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "themes/hextra"]
path = themes/hextra
url = https://github.com/tannevaled/hextra.git
[submodule "branding"]
path = branding
url = https://github.com/go-widgets/brand.git
137 changes: 137 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# docs

[![docs](https://github.com/go-widgets/docs/actions/workflows/docs.yml/badge.svg)](https://github.com/go-widgets/docs/actions/workflows/docs.yml)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-0D9488?style=flat-square)](LICENSE)

**The go-widgets documentation site**, published at
<https://go-widgets.github.io/docs/>.

A [Hugo](https://gohugo.io/) site with the [tannevaled/hextra](https://github.com/tannevaled/hextra)
fork of the [Hextra](https://github.com/imfing/hextra) theme, laid out like the
[go-fileshare documentation](https://github.com/go-fileshare/docs), with
go-widgets' branding. The content is derived from the modules' code, not only
from their READMEs, and where the two disagree the pages follow the code: a
claim here should be traceable to something a module does.

## Versions

go-widgets is nineteen modules, each released on its own, so no module's
version can label the site. A version of the documentation is **its own minor
version**:

| Source | Published under |
| --- | --- |
| branch `main`, with `params.docs.version: v0.2.0` in `hugo.yaml` | `go-widgets.github.io/docs/0.2/` |
| the newest version | also `go-widgets.github.io/docs/latest/` |

`https://go-widgets.github.io/docs/` redirects to `latest/`. Each push to `main`
replaces the directory of the version it describes, and nothing else.

A patch (`v0.2.1`) updates `0.2` in place. Raise the minor (`v0.3.0`) when the
pages of the current version should stay readable as they were: `0.3` is then
published beside `0.2`, and `latest` moves to it.

The version selector in the navbar lists every version from
`docs/versions.json` and opens the same page in the version chosen; if it is
not there, the same page in English, else that version's home.

Version 0.1 was built with MkDocs and mike before this site moved to Hugo.
Its directory on `gh-pages` is kept as it was published, and it reads the same
`versions.json` (mike's format).

## Languages

English is the default and sits at the root of each version
(`/docs/<version>/surfaces/terminal/`). French, Spanish and German are under
their code (`/docs/<version>/fr/surfaces/terminal/`, `/es/`, `/de/`). Version
0.1 is English only, and the version selector falls back to its English page.
The language switch at the foot of the sidebar opens the same page in another
language, in the same version.

To add a language `<lang>`:

1. `content/<lang>/`: a translation of every page, with the same file names
(that is how a page and its translation are paired). A translated heading
keeps the English one's anchor as an explicit id
(`## Comment Open choisit {#how-open-chooses}`), so the relrefs resolve in
every language and links into a page keep their fragment.
2. `i18n/<lang>.yaml`: the site's own strings (copyright, version).
3. A `languages.<lang>` entry in `hugo.yaml`: label, `contentDir`, weight,
`params.flag`, the translated `params.description`, and a `dateFormat`.
4. Its flag in `static/images/flags/`, from
[lipis/flag-icons](https://github.com/lipis/flag-icons) (4x3, MIT).

Declare a language only with its content: a declared language without pages
is an empty entry in the switch. A change to an English page is a change to
its three translations too.

## Layout

| Path | What |
| --- | --- |
| `content/en/` | the pages; `_index.md` is a section's own page, `weight` orders the sidebar |
| `i18n/<lang>.yaml` | the site's strings, beside the theme's |
| `static/images/flags/` | the language switch's flags (lipis/flag-icons, MIT) |
| `hugo.yaml` | `baseURL`, the documentation's version, the languages, the navbar (projects › version › theme › search › GitHub), the brand mounts |
| `layouts/_partials/custom/version-select.html` | the version selector (reads `versions.json`) |
| `layouts/_partials/favicons.html` | the brand's favicons |
| `assets/css/custom.css` | the brand teal as Hextra's primary colour, and the table headers |
| `themes/hextra` | submodule: [tannevaled/hextra](https://github.com/tannevaled/hextra), pinned to a release tag |
| `branding` | submodule: [go-widgets/brand](https://github.com/go-widgets/brand), pinned to a release tag (the mark, its PNGs and ICO, and their LICENSE) |
| `scripts/publish-version.sh` | puts one build into a `gh-pages` checkout and rewrites `versions.json`, `latest` and the root redirect |

## Writing a page

Each page starts with a title, a one-sentence description, and tags:

```yaml
---
title: "A native window"
linkTitle: "Native window"
weight: 10
description: "…"
tags: [surfaces, window]
---
```

Link to other pages with `{{< relref "/surfaces/native-window.md#how-open-chooses" >}}`:
a broken reference fails the build, and a missing anchor fails the link check.
A heading's anchor is a URL other sites link to: when a heading's words
change, keep the old anchor with an explicit `{#id}`.

A number or a version written in a page is measured at a tag, and the page
says which: a README read at a later tag than it was written at is how the
landing page came to say toolkit v0.109.0 while the tag was v0.328.0.

The page history at the foot of each page (created, modified, by whom) comes
from git: `themes/hextra/scripts/page-history.sh` writes
`data/pagehistory.json` before the build.

## Working locally

```sh
git clone --recurse-submodules https://github.com/go-widgets/docs.git
cd docs
mkdir -p data && sh themes/hextra/scripts/page-history.sh > data/pagehistory.json
hugo server --baseURL http://localhost:1313/docs/0.2/
```

then open <http://localhost:1313/docs/0.2/>. Without a `versions.json` the
selector shows only the current version. Hugo 0.146 or later (CI uses the
version pinned in the workflow); no Python, no Node.

## Publication

`.github/workflows/docs.yml`:

| Job | When | What |
| --- | --- | --- |
| `build` | pull requests, `main` | builds with `--baseURL …/docs/<version>/`, checks internal links and anchors (lychee, offline), uploads the site |
| `links` | pull requests, `main` | fetches every external URL the pages name |
| `deploy` | `main` | `scripts/publish-version.sh` into `gh-pages`, then a commit and a push; one deploy at a time |

GitHub Pages serves the `gh-pages` branch.

## Licence

BSD-3-Clause.
Loading
Loading