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
39 changes: 39 additions & 0 deletions .github/actions/install-hugo/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Install Hugo
description: Install the pinned Hugo extended build, verified by checksum before install.

# The single declaration of which generator this repo builds with.
# It used to live in both validate-task.yml and deploy-site-task.yml, with an instruction to update both and nothing enforcing it.
# A one-sided bump was silent and produced exactly the failure the pin exists to prevent: validation building the site with one generator while the deploy shipped a tree built by another, each verifying its own checksum against its own version and both passing.
# No Dependabot ecosystem tracks Hugo, so both values move by hand and there was no bot to catch the skew either.
#
# Pinned by version and by checksum rather than installed from a floating action, because the site is reproducible only if the generator is, and a minor bump can change rendered output.
# Update both values together, from the checksums file on the Hugo release.

# The pin is hardcoded below rather than exposed as inputs with defaults.
# An overridable input would let two callers pass different values and reintroduce the divergence this action exists to remove, which is the same defect one level up.
# A pin that callers cannot override is correct by construction rather than by everyone agreeing to omit the argument.
# Change it here, in one place, and every caller moves together or none does.

runs:
using: composite
steps:

# A tampered or moved artifact fails at the checksum rather than producing a wrong site.
# The extended build is asserted from the binary rather than inferred from the file name, since the name is the only thing that carried that requirement before.
- name: Install Hugo step
shell: bash
env:
HUGO_VERSION: 0.164.0
HUGO_SHA256: 8325f3653032d0fc536503691f4833dc4eb6c6be02ee62466758f3f37a7f2fcd
run: |
set -Eeuo pipefail
deb="hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
curl -sSLf -o "$deb" \
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${deb}"
echo "${HUGO_SHA256} ${deb}" | sha256sum --check --strict
sudo dpkg --install "$deb"
hugo version
hugo version | grep -q extended || {
echo "::error::Hugo reports a non-extended build, which cannot process this site's SCSS."
exit 1
}
43 changes: 29 additions & 14 deletions .github/workflows/deploy-site-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,28 @@ on:
description: The GitHub Environment to deploy to, production or staging.
required: true
type: string
outputs:
# A caller records what shipped, rather than having to read the host to find out.
# The verification below proves this id is the one answering, so it is the value a rollback names.
release-id:
description: The id of the release installed by this run.
value: ${{ jobs.deploy.outputs.release-id }}
Comment thread
ptr727 marked this conversation as resolved.
site-url:
description: The base URL the deploy was verified against.
value: ${{ jobs.deploy.outputs.site-url }}

# The transport's options are pinned rather than left to the runner's OpenSSH defaults, and declared once so the two transfers cannot drift apart.
# StrictHostKeyChecking=yes refuses an unknown or changed host key outright, where the default asks and a non-interactive runner then resolves that ambiguously.
# UserKnownHostsFile names the file the deploy key step writes, so the check reads the pinned value rather than whatever the runner image carries.
# BatchMode=yes makes every prompt an immediate failure, so a credential problem surfaces as a failed step rather than a job that hangs to its timeout.
# IdentitiesOnly=yes stops the agent offering other keys, so the deploy authenticates as the confined account or not at all.
env:
# Pinned by version and checksum, because the site is reproducible only if the generator is.
# Update both values together.
HUGO_VERSION: 0.164.0
HUGO_SHA256: 8325f3653032d0fc536503691f4833dc4eb6c6be02ee62466758f3f37a7f2fcd
SSH_TRANSPORT: >-
ssh -i ~/.ssh/deploy
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile=~/.ssh/known_hosts
-o BatchMode=yes

jobs:

Expand All @@ -22,6 +38,8 @@ jobs:
assert-environment:
name: Assert environment name job
runs-on: ubuntu-latest
# Nothing here reads the repository, so it needs no token scope at all.
permissions: {}
steps:
- name: Assert environment is known step
env:
Expand All @@ -44,6 +62,9 @@ jobs:
environment: ${{ inputs.environment }}
permissions:
contents: read
outputs:
release-id: ${{ steps.release.outputs.id }}
site-url: ${{ vars.HUGO_BASEURL }}

steps:

Expand All @@ -53,15 +74,9 @@ jobs:
with:
fetch-depth: 0

# The pin lives in the action, so the deploy and validation cannot install different generators.
- name: Install Hugo step
run: |
set -Eeuo pipefail
deb="hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
curl -sSLf -o "$deb" \
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${deb}"
echo "${HUGO_SHA256} ${deb}" | sha256sum --check --strict
sudo dpkg --install "$deb"
hugo version
uses: ./.github/actions/install-hugo

# REQUIRE_BROTLI below makes a missing binary fatal, so this keeps the build from failing.
- name: Install brotli step
Expand Down Expand Up @@ -116,7 +131,7 @@ jobs:
set -Eeuo pipefail
rsync -az --mkpath --no-g --chmod=D2755,F644 \
--link-dest="/${ENVIRONMENT}/current/" \
-e "ssh -i ~/.ssh/deploy -o IdentitiesOnly=yes" \
-e "$SSH_TRANSPORT" \
"${RUNNER_TEMP}/bundle/releases/${RELEASE_ID}/" \
"${DEPLOY_SSH_USER}@${DEPLOY_SSH_HOST}:/${ENVIRONMENT}/releases/${RELEASE_ID}/"

Expand All @@ -130,7 +145,7 @@ jobs:
run: |
set -Eeuo pipefail
rsync -a --no-recursive \
-e "ssh -i ~/.ssh/deploy -o IdentitiesOnly=yes" \
-e "$SSH_TRANSPORT" \
"${RUNNER_TEMP}/bundle/current" \
"${DEPLOY_SSH_USER}@${DEPLOY_SSH_HOST}:/${ENVIRONMENT}/"

Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ jobs:

# Staging deploys from any ref, since proving a branch before it merges is what staging is for.
# First, so a mis-dispatched production deploy fails before anything is installed or written.
# Compared against the full ref rather than ref_name, because tags and branches are separate namespaces that share a short name.
# A tag named main would satisfy a ref_name comparison while pointing at an arbitrary commit, which is a bypass of the one gate protecting production.
assert-ref:
name: Assert deploy ref job
runs-on: ubuntu-latest
# Nothing here reads the repository, so it needs no token scope at all.
permissions: {}
steps:
- name: Assert ref matches environment step
run: |
set -Eeuo pipefail
if [ "${{ inputs.environment }}" = "production" ] && [ "${{ github.ref_name }}" != "main" ]; then
echo "::error::Deploy production from main; got ${{ github.ref_name }}."
if [ "${{ inputs.environment }}" = "production" ] && [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "::error::Deploy production from main; got ${{ github.ref }}."
exit 1
fi

Expand Down
23 changes: 5 additions & 18 deletions .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ name: Validate task
on:
workflow_call:

env:
# Hugo is pinned by version and checksum rather than installed from a floating action.
# The site build is only reproducible if the generator is, and a minor Hugo bump can change output.
# Update both values together, from the checksums file on the Hugo release.
HUGO_VERSION: 0.164.0
HUGO_SHA256: 8325f3653032d0fc536503691f4833dc4eb6c6be02ee62466758f3f37a7f2fcd

jobs:

# Source-only repo: lint plus a site build gated on the URL contract, using the same configs the editor uses.
Expand All @@ -29,15 +22,16 @@ jobs:

# Doc linters run as pinned action wrappers.
# The editorconfig-checker action is install-only, so it runs via Docker instead.
# The markdown glob excludes the imported archive and the vendored theme.
# The markdown glob excludes the imported archive and the vendored theme trees.
# Neither is authored here, and .markdownlint-cli2.jsonc is carried verbatim so it cannot scope them.
# The theme exclusion reaches inside a theme directory rather than all of themes/, so a file we author about a vendored tree is still linted.
- name: Lint Markdown step
uses: DavidAnson/markdownlint-cli2-action@6bf21b07787794f89a243495939cd651942aeabe # v24.1.0
with:
globs: |
**/*.md
!content/**
!themes/**
!themes/*/**
!public/**

# The cspell gate covers README + HISTORY only.
Expand Down Expand Up @@ -77,16 +71,9 @@ jobs:
done
python3 -c 'import yaml,sys; yaml.safe_load(open("hugo.yaml"))'

# Pinned by checksum, so a tampered or moved artifact fails here rather than producing a wrong site.
# The pin lives in the action, so validation and the deploy cannot install different generators.
- name: Install Hugo step
run: |
set -Eeuo pipefail
deb="hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
curl -sSLf -o "$deb" \
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${deb}"
echo "${HUGO_SHA256} ${deb}" | sha256sum --check --strict
sudo dpkg --install "$deb"
hugo version
uses: ./.github/actions/install-hugo

# --panicOnWarning is the real gate.
# Two PaperMod templates are overridden in layouts/ precisely so it can stay on.
Expand Down
36 changes: 36 additions & 0 deletions themes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Vendored themes

This directory holds third-party theme source, copied in rather than fetched by a manager. It sits outside `PaperMod/` deliberately, so replacing that directory wholesale on an update does not take this record with it.

Vendoring is the decision; not recording what was vendored was the gap. Without an upstream ref there is no way to ask what changed upstream, whether a fix landed, or whether a local edit is still needed, and a 125-file copy is a large surface to carry blind.

## PaperMod

| | |
| --- | --- |
| Upstream | <https://github.com/adityatelange/hugo-PaperMod> |
| Commit | `154d006e0182dfc7da38008323976b02e6bfab4a` |
| Committed upstream | 2026-05-10 |
| Describes as | `v8.0-138-g154d006` |
| License | MIT, retained at `PaperMod/LICENSE` |

The commit was recovered by matching all 125 tracked blobs against upstream history rather than by reading a version marker, since the copy carries none. Every file matches that commit exactly except the two below, so the identification is not approximate.

### Local edits

Both sit in extension points the theme documents for this purpose, so neither is a fork of theme logic.

| File | Edit |
| --- | --- |
| `PaperMod/assets/css/extended/blank.css` | The theme's custom-CSS slot, which ships empty. Carries the Lexend body font and the `gallery` and `gallery-cols-*` rules the gallery shortcode needs. |
| `PaperMod/layouts/_partials/extend_head.html` | The theme's head-extension partial, which ships empty. Carries the Google Fonts preconnect and stylesheet links for Lexend. |

Both could live outside the vendored tree instead: Hugo resolves a project's own `assets/css/extended/` and `layouts/_partials/` ahead of the theme's, so moving them would make an update a clean directory replace with nothing to reapply. Worth doing at the next update rather than as a change of its own.

Separately, `layouts/` at the repository root already overrides two theme templates, for the reason recorded in [`TODO.md`](../TODO.md): PaperMod uses APIs Hugo deprecated in 0.158, and `--panicOnWarning` would otherwise fail on the theme rather than on content. Whether those overrides are still needed is answerable by diffing against the commit above, which is what this record exists for.

## Updating

Compare against the recorded commit first, so the local edits above are known before anything moves. Replace `PaperMod/` with the new upstream tree, reapply the two edits (or move them out, per the note above), update the table here, and confirm the site still builds under `--panicOnWarning`, which is the gate the theme has failed before.

No bot watches this. `.github/dependabot.yml` covers GitHub Actions only, since a vendored copy has no manifest to track, so an update is a deliberate act.