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
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Keep the SHA-pinned GitHub Actions current. Dependabot bumps the commit pin AND the trailing
# "# vX.Y.Z" comment together, and opens PRs for any security advisories affecting an action we use.
#
# Scope is github-actions ONLY (#11): this repo is a static Hugo site with no app dependencies —
# no package.json, go.mod, pip, or docker to track. Hugo itself is installed by version-pinned
# `wget` of a release .deb in the workflows (HUGO_VERSION), which Dependabot cannot see, so Hugo
# bumps stay manual (see #15).
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/" # github-actions ecosystem watches .github/workflows/
schedule:
interval: "weekly"
commit-message:
prefix: "ci" # -> "ci(deps): bump actions/checkout ..."
include: "scope"
labels:
- "infra"
groups:
# One rollup PR for all action bumps rather than one-per-action — low-noise for a repo this small.
github-actions:
patterns:
- "*"
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ jobs:
HTMLTEST_VERSION: "0.17.0"
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false # zizmor: artipacked

- name: Install Hugo (extended)
run: |
Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ on:
- cron: "17 6 * * *"
workflow_dispatch:

# Least-privilege floor: the build job only reads the tree. Only the deploy job
# needs pages:write + id-token:write, scoped to it below. (zizmor: excessive-permissions)
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment; don't cancel an in-progress production deploy.
concurrency:
Expand All @@ -35,13 +35,14 @@ jobs:
https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
submodules: recursive
fetch-depth: 0
persist-credentials: false # zizmor: artipacked
- name: Setup Pages
id: pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
with:
enablement: true
- name: Refresh release versions (best-effort)
Expand All @@ -52,13 +53,16 @@ jobs:
env:
HUGO_ENVIRONMENT: production
TZ: UTC
# Pass the Pages URL through env rather than expanding it inline in the
# script (avoids template-injection; zizmor: template-injection).
BASE_URL: ${{ steps.pages.outputs.base_url }}
run: |
hugo \
--gc \
--minify \
--baseURL "${{ steps.pages.outputs.base_url }}/"
--baseURL "${BASE_URL}/"
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: ./public

Expand All @@ -68,7 +72,11 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
# Only this job touches Pages — grant the write scopes here, not repo-wide.
permissions:
pages: write
id-token: write
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
75 changes: 75 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Security

# Supply-chain & secrets gates (#11):
# - gitleaks: scan the full git history for committed secrets (tokens, credentials) on every
# push and PR.
# - zizmor: static-audit the GitHub Actions workflows themselves (template injection, over-broad
# GITHUB_TOKEN, unpinned actions, credential persistence) AND cross-reference the actions we
# pin against the GitHub Advisory Database (online audit).
# Dependabot (github-actions) lives in .github/dependabot.yml; the matching gitleaks pre-commit
# hook lives in .pre-commit-config.yaml.

on:
push:
branches: [main]
pull_request:
# Re-audit on a schedule so a newly-published advisory against an action we pin trips the gate
# even during quiet periods with no pushes — the online zizmor audit is time-varying by design.
schedule:
- cron: "0 7 * * 1" # Mondays 07:00 UTC

# Both jobs only read the tree to scan it. Pin the floor to read-only (zizmor: excessive-permissions).
permissions:
contents: read

jobs:
gitleaks:
name: Secret scan (gitleaks)
runs-on: ubuntu-24.04
# The weekly tick exists for zizmor's advisory re-audit; history doesn't change between pushes,
# so there's nothing new for gitleaks to scan on a schedule.
if: github.event_name != 'schedule'
env:
# Pinned + checksum-verified — reproducible and immune to runner-image drift. Keep
# GITLEAKS_VERSION in lockstep with .pre-commit-config.yaml.
GITLEAKS_VERSION: "8.30.1"
GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0 # scan EVERY commit, not just the tip — a secret is still a leak once pushed
persist-credentials: false # zizmor: artipacked
- name: Install pinned gitleaks
run: |
set -euo pipefail
tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" -o "$tarball"
echo "${GITLEAKS_SHA256} ${tarball}" | sha256sum -c -
tar -xzf "$tarball" gitleaks
sudo install gitleaks /usr/local/bin/gitleaks
gitleaks version
# Full-history scan with the built-in ruleset. --redact keeps any match out of the public logs;
# the job still fails (non-zero exit) so a leak blocks the merge.
- name: Scan git history for secrets
run: gitleaks git . --redact --no-banner --verbose

zizmor:
name: Workflow audit (zizmor)
runs-on: ubuntu-24.04
env:
ZIZMOR_VERSION: "1.25.2"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # zizmor: artipacked
# pipx is preinstalled on ubuntu-24.04.
- name: Install pinned zizmor
run: pipx install "zizmor==${ZIZMOR_VERSION}"
# Online audits ON (zizmor's default): GH_TOKEN lets the `known-vulnerable-actions` audit query
# the GitHub Advisory Database, so a CVE disclosed against an action we pin fails the gate. The
# built-in token (read-only here) is enough — advisory data is public; it's only for API access.
# This complements Dependabot: zizmor blocks the merge, Dependabot opens the bump.
- name: Audit GitHub Actions workflows
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: zizmor .github/workflows/
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Pre-commit hooks for the site. Install once per clone:
#
# pipx install pre-commit # or: pip install pre-commit
# pre-commit install
#
# Scope here is the secret-scanning gate from #11: gitleaks, pinned to the SAME version CI runs
# (.github/workflows/security.yml) so a leak is caught locally before it's ever pushed.
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1 # keep in lockstep with GITLEAKS_VERSION in .github/workflows/security.yml
hooks:
- id: gitleaks
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ not in the templates. See the [README](README.md#where-the-content-lives) for th
```
4. Update the README or other docs for any user-facing change.

## Secret scanning

CI runs [gitleaks](https://github.com/gitleaks/gitleaks) over the full history on every push and PR,
so an accidentally committed token or credential blocks the merge. Catch it locally first by
installing the pre-commit hook (it runs the same pinned gitleaks on staged changes):

```bash
pipx install pre-commit # or: pip install pre-commit
pre-commit install
```

## Opening a pull request

- Target the `main` branch and fill out the PR template.
Expand Down