diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index e6984135..e2f61ad0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -19,6 +19,23 @@ jobs: - name: Read the Hugo version run: echo "HUGO_VERSION=$(tr -d '[:space:]' < .hugo-version)" >> "$GITHUB_ENV" + - name: Restore the generated images + # A cold build resizes more than 400 images, about 90 seconds of CPU + # for output that is the same every time. Hugo names each generated + # file after a hash of the source image and the processing options, + # so a cached file is used only when both match and a stale entry is + # never served. The key follows the image sources; when one changes, + # restore-keys brings the previous cache and only the new image is + # resized. A pull request can restore a cache saved on its base + # branch, and this workflow runs on edition, so the cache it saves + # is the one a new pull request starts from. + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: resources/_gen + key: hugo-gen-${{ hashFiles('themes/pybcn_theme/assets/images/**') }} + restore-keys: | + hugo-gen- + - name: Setup Hugo uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fadd92b9..7e1e84ed 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,21 @@ jobs: - name: Read the Hugo version run: echo "HUGO_VERSION=$(tr -d '[:space:]' < .hugo-version)" >> "$GITHUB_ENV" + - name: Restore the generated images + # A cold build resizes more than 400 images, about 90 seconds of CPU + # for output that is the same every time. Hugo names each generated + # file after a hash of the source image and the processing options, + # so a cached file is used only when both match and a stale entry is + # never served. The key follows the image sources; when one changes, + # restore-keys brings the previous cache and only the new image is + # resized. + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: resources/_gen + key: hugo-gen-${{ hashFiles('themes/pybcn_theme/assets/images/**') }} + restore-keys: | + hugo-gen- + - name: Setup Hugo uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 with: @@ -55,6 +70,21 @@ jobs: - name: Read the Hugo version run: echo "HUGO_VERSION=$(tr -d '[:space:]' < .hugo-version)" >> "$GITHUB_ENV" + - name: Restore the generated images + # A cold build resizes more than 400 images, about 90 seconds of CPU + # for output that is the same every time. Hugo names each generated + # file after a hash of the source image and the processing options, + # so a cached file is used only when both match and a stale entry is + # never served. The key follows the image sources; when one changes, + # restore-keys brings the previous cache and only the new image is + # resized. + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: resources/_gen + key: hugo-gen-${{ hashFiles('themes/pybcn_theme/assets/images/**') }} + restore-keys: | + hugo-gen- + - name: Setup Hugo uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 with: @@ -64,7 +94,16 @@ jobs: - name: Build # A root-relative baseURL turns every internal link into "/path/", so # lychee resolves it inside this build and not on the live site. - run: hugo --minify -d public --baseURL / + # -D builds the drafts: the canary fixture that bin/check-rendered + # reads is three draft pages. The deploy workflow builds without -D, + # so the fixture never reaches the live site. + run: hugo --minify -D -d public --baseURL / + + - name: Check the built pages for dangerous markup + # Reads what Hugo wrote, so a template defect is caught here even + # when every content file is clean. Standard library only: the + # runner's python3 is enough. + run: bin/check-rendered public - name: Check links uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 @@ -73,7 +112,10 @@ jobs: # every absolute link in a local file as an error. # --exclude-path: the frozen 2016-2019 snapshots under archives/ # keep their dead links on purpose. - # --exclude: hosts that answer bots with 400, 403, or 999. + # --exclude: hosts that answer bots with 400, 403, or 999, and + # canary.invalid, the host the canary fixture names as text. + # lychee reads URLs out of text nodes too, and .invalid never + # resolves. args: >- --no-progress --exclude-all-private @@ -83,7 +125,8 @@ jobs: --exclude 'twitter\.com' --exclude '^https?://(www\.)?x\.com/' --exclude 'linkedin\.com' - --exclude 'pybcn\.slack\.com' + --exclude 'pybcn\.slack\.com' + --exclude 'canary\.invalid' 'public/**/*.html' # External sites rate-limit. Keep the report in the job summary and # do not turn the check red. diff --git a/README.md b/README.md index 0c17bfbc..b17da5d2 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,47 @@ Once the site was archived, you can create a new link in the navigational menu u ## Information for developers +### Collaborate + +A change to this site goes through a pull request. Nothing is pushed to +`edition` directly. + +1. Clone the repository. `git clone --single-branch --branch edition` is enough + and skips the built site, which lives on its own branch. +2. Run `bin/install`. It downloads the pinned Hugo binary into `bin/hugo` and + verifies its checksum. You do not need Python, Go, or npm for this step. +3. Run `bin/serve` to see the site at `http://localhost:1313` while you work. +4. Make the change. +5. Run the three checks locally, so you find what the pull request would find: + + ``` + pip install pyyaml + bin/check-content + bin/check-html-safety + bin/hugo --minify -D -d public && bin/check-rendered + ``` + +6. Open the pull request against `edition`. + +The `pr-checks` workflow then runs the same checks on your branch, plus a build +and a link check. **All of them have to pass**, and one approving review is +needed before the pull request can be merged. The paths listed in +`.github/CODEOWNERS` also request a review from the web team automatically. + +What each check is for: + +| Check | Fails when | +|---|---| +| Build the site | Hugo cannot build, or emits a warning | +| `bin/check-content` | Front matter does not parse, a person `id` does not match its filename, an id is duplicated, a declared photo is missing, or an event references a person or sponsor that does not exist | +| `bin/check-html-safety` | Content carries raw HTML that turns a content change into script execution, a redirect, a credential prompt, or a page overlay | +| `bin/check-rendered` | The built pages carry that same markup, which catches a template that produces it even when no content file does | +| Check the links | Reported, never blocking, because external sites rate-limit | + +A merge to `edition` deploys the site. The `github-pages` workflow builds it and +publishes the result, so a change is live within a few minutes of the merge. + + ### Content safety check `bin/check-html-safety` scans `content/` and fails if it finds raw HTML that @@ -115,10 +156,52 @@ URL the element loads, so it permits one known embed and not any later one in the same file, and the file that carries it needs a rule in `.github/CODEOWNERS`, or any contributor can edit the allowed snippet. +### Rendered page check + +`bin/check-rendered` reads the pages Hugo built and fails on dangerous markup +in them: an `on...=` handler on any element, a `javascript:`, `vbscript:` or +`data:` URL in any attribute a browser loads from or navigates to (entities +decoded, the way a browser does), an inline `