Skip to content

ci: follow-up workflow hardening (action updates, hashed deps lock, fork guard, perms default)#300

Merged
tomaz-lc merged 5 commits into
masterfrom
harden-ci-followup
Jul 10, 2026
Merged

ci: follow-up workflow hardening (action updates, hashed deps lock, fork guard, perms default)#300
tomaz-lc merged 5 commits into
masterfrom
harden-ci-followup

Conversation

@tomaz-lc

@tomaz-lc tomaz-lc commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Details

Follow-up CI/security hardening pass on the GitHub Actions workflows, continuing the review after #294. It updates the first-party actions to their current majors, pins the Python documentation dependencies with a hashed lock file so builds (including the production Pages deploy) are reproducible and tamper-evident, and closes two small consistency gaps: a missing fork guard and a missing workflow-level permissions default.

There are no documentation content changes; the resolved dependency versions are unchanged, so the built site output is unchanged.

What's included

  • Update first-party actions to latest majors (kept as major tags, not SHA-pinned): actions/checkout v6 to v7, actions/cache v5 to v6, actions/setup-go v5 to v6, actions/configure-pages v5 to v6, actions/upload-pages-artifact v4 to v5, actions/deploy-pages v4 to v5, actions/github-script v8 to v9, and actions/upload-artifact v4 to v7 in docs-preview (link-checker was already v7). setup-python (v6) and download-artifact (v8) were already latest. The SHA-pinned third-party actions (lychee, markdownlint) are unchanged. All bumps are node-runtime upgrades with no breaking input changes for how they are used here.
  • Hashed dependency lock: requirements.in now holds the human-edited direct dependencies (the former ranges) and requirements.txt is generated from it with pip-compile --generate-hashes --strip-extras, pinning every direct and transitive package to an exact version with sha256 hashes. Installing it puts pip in --require-hashes mode automatically. lexer-tests installs the lock on its own and then pytest (pinned 9.1.1) separately, since pip refuses to mix hashed and unhashed requirements in one command.
  • Fork guard on check-markdown: the one PR-triggered job in link-checker.yml that lacked the fork guard the other jobs have now guards against fork PRs (low-risk, since markdownlint runs no repository JS, but added for consistency).
  • Least-privilege default on deploy-pages.yml: added a top-level permissions: contents: read so any future job added without its own block defaults to read-only; the existing job-level blocks (build contents: read; deploy pages: write + id-token: write) are unchanged.

Blast radius / isolation

Changes are limited to .github/workflows/*, requirements.in, and requirements.txt. No documentation content, no MkDocs config, no site behavior change. What is affected: the action versions across all six workflows, the dependency-install step of the build/deploy workflows (now from the lock), the link-checker check-markdown job (fork guard), and the deploy-pages permission default. Not affected: documentation content, the rendered site output, and the lexer/snippet/numbering logic.

Notable contracts

  • requirements.txt is now a generated lock, not hand-edited. To change dependencies, edit requirements.in and regenerate with the pip-compile command in the file header. Installs enforce hashes, so an unexpected or tampered artifact fails the install instead of entering a build.
  • No wire-format, API, or data-schema changes.

Performance characteristics

Negligible. Hashed installs are the same speed as before; the action bumps are node-runtime updates. No change to build time or site size.

How this was verified

  • All six workflows parse as valid YAML; every actions/* reference confirmed at its current latest major.
  • A clean virtualenv installs from the hashed lock (hash enforcement active), mkdocs build --strict succeeds, and the 57 lexer tests pass with the pinned pytest.
  • deploy-pages permissions confirmed: top-level contents: read, deploy job still pages: write + id-token: write.

Things to watch after merge

  • The deploy-pages.yml action bumps run only on push-to-master (that workflow does not run on pull requests), so they are not exercised by this PR's CI; they first execute on the production Pages deploy after merge. Each bump was checked against its release notes and carries no breaking input change, and a failed Pages deploy leaves the currently live site in place (recoverable). Worth watching the first post-merge deploy, or manually running the workflow via workflow_dispatch.
  • Likewise the link-checker.yml bumps (cache v6, github-script v9) and the new check-markdown fork guard are not exercised by this PR, because that workflow is path-filtered to docs/** / mkdocs.yml (untouched here); they first run on the next docs-touching PR or the weekly schedule. They are YAML-validated and use the same latest majors already exercised by the other jobs. This PR's CI (docs-preview, lexer-tests, snippet-tests) does exercise checkout v7, setup-go v6, upload-artifact v7, and the hashed-lock install.

Rollback / revert

Workflow-only change: revert the PR to restore the previous action versions and dependency ranges. If a single action bump misbehaves, revert just that commit or pin the affected action back one major.

Not included

Dependabot (a github-actions / pip / gomod update config) was intentionally left out of this PR; without it these action and dependency pins are bumped manually. Can be added separately if desired.

Related PRs

🤖 Generated with Claude Code

tomaz-lc and others added 4 commits July 10, 2026 09:48
Bump the actions/* actions used across the workflows to their current
major releases (node runtime upgrades; no breaking input changes for how
they are used here):

- actions/checkout v6 -> v7
- actions/cache v5 -> v6
- actions/setup-go v5 -> v6
- actions/configure-pages v5 -> v6
- actions/upload-pages-artifact v4 -> v5
- actions/deploy-pages v4 -> v5
- actions/github-script v8 -> v9
- actions/upload-artifact v4 -> v7 (docs-preview; link-checker was already v7)

setup-python (v6) and download-artifact (v8) are already on the latest
major. First-party actions stay pinned by major tag (not SHA); the
SHA-pinned third-party actions (lychee, markdownlint) are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the loosely-ranged requirements.txt with a compiled, fully pinned
and hashed lock so documentation builds (including the production Pages
deploy) are reproducible and tamper-evident:

- requirements.in holds the human-edited direct dependencies (the former
  ranges) and is the source of truth.
- requirements.txt is now generated from it with
  `pip-compile --generate-hashes --strip-extras`, pinning every direct and
  transitive package to an exact version with sha256 hashes. Installing it
  puts pip in --require-hashes mode automatically, so a compromised or
  unexpected artifact fails the install instead of silently entering a build.

lexer-tests installs the lock on its own and then pytest separately, since
pip refuses to mix hashed and unhashed requirements in one command; pytest
is pinned (9.1.1) but kept out of the docs lock as a test-only tool.

Verified: a clean venv installs from the lock, `mkdocs build --strict`
succeeds, and the 57 lexer tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every other pull-request-triggered job in link-checker.yml (check-links,
comment-links) already guards on the PR head being a branch in this
repository, not a fork. check-markdown was the lone exception. It is
low-risk (markdownlint has no custom rules, so it runs no repository JS,
with a read-only token and no secrets), but add the same guard so all
PR-triggered jobs are consistent and fork PRs run nothing here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deploy-pages.yml set permissions only at the job level (build: contents:read;
deploy: pages:write + id-token:write) with no workflow-level default, so a
future job added without its own block would inherit the repository default
token scope. Add a top-level `permissions: contents: read` so the safe default
applies workflow-wide, matching every other workflow in the repo. The existing
job-level blocks are unchanged and still take precedence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tomaz-lc tomaz-lc marked this pull request as ready for review July 10, 2026 10:43
…steps

Add a Local Development section covering how to install the build
dependencies and preview the site, and document that requirements.txt is a
generated, pinned + hashed lock: edit requirements.in and regenerate with
`pip-compile --generate-hashes --strip-extras` rather than hand-editing the
lock. Commit both files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tomaz-lc tomaz-lc merged commit bc386bb into master Jul 10, 2026
4 checks passed
@tomaz-lc tomaz-lc deleted the harden-ci-followup branch July 10, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants