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
27 changes: 20 additions & 7 deletions .github/actions/setup-hugo/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,32 @@ runs:
with:
node-version: '22'

# hugo_stats.json is cached deliberately: it is gitignored, and without it
# every run pays bin/hugo-build's ~52s PurgeCSS warm-up double-build. The
# key hashes config/** (the real config location - the old key hashed
# nonexistent root hugo.toml/hugo.dev.toml, so config-only changes reused
# a stale resources/_gen).
# Two separate caches on purpose:
#
# 1. hugo_stats.json - EXACT key only, NO restore-keys. The warm-up skip
# in _hugo.yml/bin/hugo-build trusts a non-empty stats file; a
# restore-keys partial hit would restore a STALE stats file from an
# older tree and PurgeCSS would purge classes that are live in THIS
# tree (the 2026-07-19 .sr-only incident class). An exact hit means
# the source tree is identical, so the stats are valid; any change
# misses and pays the ~52s warm-up instead of risking a broken deploy.
- name: Cache hugo_stats.json (exact tree match only)
uses: actions/cache@v5
with:
path: hugo_stats.json
key: ${{ runner.os }}-hugo-stats-${{ inputs.environment }}-${{ hashFiles('config/**', 'postcss.config.js', 'package.json', '**/bun.lockb', 'themes/**', 'layouts/**', 'content/**') }}
Comment on lines +64 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 'setup-hugo|hugo-version:|BUILD_DRAFTS:|hugo\.toml|build-drafts' .github || true
fd -t f -a 'hugo\.(toml|yaml|yml|json)$' .

Repository: jetthoughts/jetthoughts.github.io

Length of output: 10301


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== setup-hugo action =="
cat -n .github/actions/setup-hugo/action.yml | sed -n '1,110p'

echo "== test workflow setup-hugo caller and bin/hugo-build references =="
cat -n .github/workflows/test.yml | sed -n '70,95p'

echo "== bin/hugo-build draft/env handling =="
fd -t f -a 'hugo-build|hugo\.toml' . | sed 's#^\./##'
if [ -f bin/hugo-build ]; then
  cat -n bin/hugo-build | sed -n '1,220p'
fi

echo "== relevant docs gate references =="
fd -t f -a 'ci-gates\.md' .
if [ -f .okf/build/ci-gates.md ]; then
  cat -n .okf/build/ci-gates.md | sed -n '1,120p'
fi

echo "== search for buildDrafts / BUILD_DRAFTS / hugo_stats =="
rg -n -C 4 'BUILD_DRAFTS|buildDrafts|hugo_stats\.json|bin/hugo-build/.github/actions/setup-hugo' .

Repository: jetthoughts/jetthoughts.github.io

Length of output: 50391


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== bin/qtest outline/relevant refs =="
ast-grep outline bin/qtest --view expanded || true
rg -n -C 5 'hugo_stats|BUILD_DRAFTS|setup-hugo|qtest|bin/hugo-build|hugo build' bin/qtest bin/setup-test-env test || true
if [ -f bin/qtest ]; then
  cat -n bin/qtest | sed -n '1,220p'
fi

echo "== root config and theme draft configs =="
for f in hugo.toml config/_default/hugo.toml config/development/hugo.toml; do
  [ -f "$f" ] && { echo "--- $f"; cat -n "$f" | sed -n '1,90p'; }
done

echo "== theme-wide buildDrafts settings =="
rg -n 'buildDrafts|drafts|enabled = false|writeStats' --glob '*.toml' --glob '*.hbs' --glob '*.html' themes config content bin .github || true

Repository: jetthoughts/jetthoughts.github.io

Length of output: 28945


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== bin/build-if-stale and env/build details =="
cat -n bin/build-if-stale | sed -n '1,220p'

echo "== test workflow cache/build sequence around setup-hugo =="
cat -n .github/workflows/test.yml | sed -n '1,150p'

echo "== static cache-key model: does key distinguish draft env?"
python3 - <<'PY'
import re
line='key: ${{ runner.os }}-hugo-stats-${{ inputs.environment }}-${{ hashFiles(\'config/**\', \'postcss.config.js\', \'package.json\', \'**/bun.lockb\', \'themes/**\', \'layouts/**\', \'content/**\') }}'
print(line.strip())
print("BUILD_DRAFTS appears in key:", "BUILD_DRAFTS" in line or "buildDrafts" in line or "build-drafts" in line)
print("hugo-version appears in key:", "inputs.hugo-version" in line)
print("root hugo.toml in hashFiles:", bool(re.search(r"hashFiles\([^)]*'hugo\.toml'", line)))
print("buildDrafts env used by Hugo output:", open("bin/hugo-build").read().count("BUILD_DRAFTS"))
PY

echo "== content draft post that requires BUILD_DRAFTS =="
rg -n -C 4 'draft\s*:\s*true|draft: true|codeblock-styles-fixture' config content archives themes --glob '*.md' --glob '*.json' --glob '*.toml'

Repository: jetthoughts/jetthoughts.github.io

Length of output: 11300


Key hugo_stats.json by draft scope too.

The current key does not include BUILD_DRAFTS, but test builds run Hugo with --buildDrafts. If a production warm tree cache restores a stats file, the draft fixture can be served without regenerating it, which may make PurgeCSS miss draft-only classes. Add a draft-mode/cache-mode token to the key and make the visual-cache documentation match that scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/setup-hugo/action.yml around lines 64 - 68, Update the
hugo_stats.json cache key in the setup workflow to include a token derived from
BUILD_DRAFTS, distinguishing draft and production cache entries. Update the
related visual-cache documentation to describe the same draft-scoped cache
behavior.


# 2. Hugo's own caches - restore-keys are safe here (Hugo content-hashes
# resources/_gen entries internally), and partial hits still save real
# time. _dest is deliberately NOT cached: it uploaded ~1-2 GB per job,
# evicting every other cache from the 10 GB repo quota, and the build
# step regenerates it anyway.
- name: Cache Hugo build
uses: actions/cache@v5
with:
path: |
/tmp/hugo_cache
resources/_gen
_dest
hugo_stats.json
key: ${{ runner.os }}-hugo-${{ inputs.environment }}-${{ hashFiles('config/**', 'postcss.config.js', 'package.json', '**/bun.lockb', 'themes/**', 'layouts/**', 'content/**') }}
restore-keys: |
${{ runner.os }}-hugo-${{ inputs.environment }}-
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
paths:
- 'content/**'
- 'themes/**/layouts/**'
# root-level template overrides move links just like theme layouts
- 'layouts/**'
- 'config/**'
- 'data/**'
- 'lychee.toml'
Expand Down Expand Up @@ -43,6 +45,11 @@ defaults:
jobs:
link_check:
name: Broken Internal Links
# Sync fan-out gate: "Sync articles" completes every 10 min during the
# day, but most runs commit nothing. For workflow_run events, only crawl
# when the sync actually pushed (master head moved past the sha the sync
# ran on). github.sha for workflow_run = current default-branch head.
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_sha != github.sha) }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -55,7 +62,12 @@ jobs:
ruby-version: '4.0'
bundler-cache: true

# setup only - rake test:links runs its own production build via
# bin/hugo-build; the composite's default build made this job build
# the whole site TWICE and blow its 10-minute timeout on cold caches.
- uses: ./.github/actions/setup-hugo
with:
build: 'false'

- uses: taiki-e/install-action@v2
with:
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ defaults:
run:
shell: bash

# Sync fan-out gate (both jobs): "Sync articles" completes every 10 min
# during the day, but most runs commit nothing - deploying + testing an
# unchanged tree ~84x/day is pure waste. For workflow_run events, run only
# when the sync actually pushed (master head moved past the sha the sync ran
# on). github.sha for workflow_run = current default-branch head.
jobs:
build_and_deploy:
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_sha != github.sha) }}
uses: ./.github/workflows/_hugo.yml

unit_tests:
name: Unit Tests
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_sha != github.sha) }}
Comment on lines +36 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 12 \
  'workflow_run|Sync articles|push:|workflows:|git push|git commit' \
  .github/workflows

Repository: jetthoughts/jetthoughts.github.io

Length of output: 13609


🌐 Web query:

GitHub Actions workflow_run event github.event.workflow_run.head_sha current branch HEAD

💡 Result:

When using the workflow_run event in GitHub Actions, the workflow executes in the context of the repository's default branch, not the branch that triggered the original workflow [1][2]. Therefore, standard environment variables like GITHUB_REF or GITHUB_SHA will reflect the default branch rather than the branch of the triggering commit [2][3]. To access information about the original workflow run that triggered the current event, you must use the github.event.workflow_run context [2]. Key points for accessing branch and commit information: 1. Accessing the Branch Name: You can obtain the name of the branch that triggered the original workflow by using ${{ github.event.workflow_run.head_branch }} [2][3]. 2. Accessing the Commit SHA: You can obtain the SHA of the commit that triggered the original workflow by using ${{ github.event.workflow_run.head_sha }} [4][5]. 3. Checking Out the Correct Code: Because the workflow runs on the default branch by default, you must explicitly configure your checkout step to use the original branch or SHA if you need to access the code from that specific commit [2][3]. Example usage with actions/checkout: - uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha }} Using head_sha is generally safer than head_branch for checkout, as it ensures you are working with the exact state of the code that triggered the original workflow, preventing issues if new commits have been pushed to that branch in the interim [5].

Citations:


🌐 Web query:

GitHub workflow_run event github.sha head_sha head_commit

💡 Result:

In a GitHub Actions workflow triggered by the workflow_run event, the standard github.sha context does not represent the commit that triggered the original workflow [1]. Instead, github.sha defaults to the last commit on the default branch of the repository where the workflow is running [2][3]. To access the SHA of the commit that triggered the original workflow, you must use the event payload available within the github.event context [4][5]. Specifically, you should use: ${{ github.event.workflow_run.head_sha }} This field, head_sha, contains the SHA of the commit that triggered the workflow run that this workflow is reacting to [4][1]. It is essential to use this specific property because the workflow_run event context differs significantly from events like push, where github.sha directly points to the triggering commit [4][5]. Note that head_commit is also present in the webhook payload for some events (like push), but for workflow_run, the head_sha within the workflow_run object is the standard and correct way to retrieve the triggering commit's identifier [4][6].

Citations:


Gate on whether Sync articles pushed, not on any default-branch movement.

github.event.workflow_run.head_sha is the commit that the Sync articles run was based on, while github.sha is the current default-branch head in the workflow context. If an unrelated commit lands after the sync starts, the condition passes even when the sync performed no commit-push. Move deploy/test/link-check runs behind an explicit pushed-result signal, or start this workflow from the sync’s push, and apply the same rule in .github/workflows/link-check.yml:24-53.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 36 - 41, Replace the workflow_run
head_sha comparison in the publish workflow’s deploy and unit_tests jobs with an
explicit signal that Sync articles pushed a commit, or trigger this workflow
directly from the sync push. Apply the same gating rule to the corresponding
runs in link-check.yml, preserving execution for normal non-workflow_run
triggers.

runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down
26 changes: 23 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ on:
pull_request:
paths:
- 'themes/**'
# root-level template/asset overrides change rendering just like
# their themes/ counterparts
- 'layouts/**'
- 'assets/**'
- 'config/**'
- 'postcss.config.js'
- 'test/fixtures/screenshots/**'
Expand Down Expand Up @@ -55,9 +59,13 @@ jobs:
continue-on-error: ${{ github.event_name == 'pull_request' }}

steps:
# PR events use the default checkout (the PR *merge* commit): testing
# the head branch alone misses conflicts with the base and breaks on
# fork PRs (head_ref doesn't exist in this repo). Record dispatches
# check out the real branch ref so the baseline commit can be pushed.
- uses: actions/checkout@v7
with:
ref: ${{ github.head_ref || github.ref }}
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || '' }}

- uses: snap-diff/snap_diff-capybara/.github/actions/setup-ruby-and-dependencies@5ee298fbc3c1e070c78e3d83862ffd3228204214
with:
Expand Down Expand Up @@ -95,18 +103,30 @@ jobs:
PRECOMPILED_ASSETS: '1'
HUGO_DEFAULT_PATH: _dest/public-test

# test:system, not the full `rake test`: recording only rewrites
# screenshot baselines, and a failing unit test used to abort the run
# AFTER 15+ min of recording, discarding every recorded PNG.
- name: Record baselines
id: record
if: ${{ github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
run: FORCE_SCREENSHOT_UPDATE=true bundle exec rake test
run: FORCE_SCREENSHOT_UPDATE=true bundle exec rake test:system
env:
PRECOMPILED_ASSETS: '1'
HUGO_DEFAULT_PATH: _dest/public-test
# Record mode intentionally dirties the fixtures as it runs.
ALLOW_DIRTY_SCREENSHOTS: '1'

# always(): freshly recorded baselines must be committed even when a
# test failed mid-run (a red test does not invalidate the OTHER pages'
# recordings). Gated on the record step actually having run, and the
# commit is a no-op when nothing changed.
- name: Commit updated baselines
if: ${{ github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
if: ${{ always() && steps.record.outcome != 'skipped' && github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
Comment on lines +119 to +124

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate test.yml =="
fd -a 'test\.yml$' . | sed 's#^\./##' || true

echo "== relevant workflow excerpt =="
if [ -f .github/workflows/test.yml ]; then
  nl -ba .github/workflows/test.yml | sed -n '90,140p'
fi

echo "== workflow_dispatch inputs/steps references =="
if [ -f .github/workflows/test.yml ]; then
  rg -n "update-baselines|steps\.record|record:|cancelled\(\)|pullRequest|system|commit updated baselines" .github/workflows/test.yml
fi

echo "== GitHub Actions cancel/always/outcome docs probe =="
python3 - <<'PY'
# Read-only semantic probe: demonstrate the workflow expression logic
# from the referenced condition without importing repository code.
import json, pathlib
p = pathlib.Path('.github/workflows/test.yml')
print('exists', p.exists())
if p.exists():
    text = p.read_text()
    found = False
    for i,line in enumerate(text.splitlines(),1):
        if 'if: ${{ always() && steps.record.outcome !=' in line:
            found = True
            print(f'line {i}: {line.strip()}')
            print('contains cancelled guard:', '!cancelled()' in line)
            print('contains skipped guard:', '!= '\''skipped'\'' in line or "!= '\''skipped'\''" in line')
if not found:
    print('condition not found')
PY

Repository: jetthoughts/jetthoughts.github.io

Length of output: 311


🌐 Web query:

GitHub Actions contexts outcome cancelled status cancellation status always() cancelled()

💡 Result:

In GitHub Actions, the cancelled() function is a status check function used within the if conditional to determine if the workflow run or a specific job has been cancelled [1]. Key concepts and behaviors: 1. cancelled function: When used in an if conditional, cancelled() returns true if the workflow run or job was cancelled [1]. This is commonly used to execute cleanup steps or report status after a cancellation [1]. 2. always function: The always() function is used in if conditionals to ensure that a step or job runs regardless of the outcome of previous steps or the status of the job [2][1]. A step with if: always() will execute even if the workflow was cancelled or a previous step failed [1]. 3. Interaction with Cancellation: When a workflow is cancelled, GitHub Actions re-evaluates if conditions for currently running jobs and steps [1]. Jobs or steps without an explicit if condition effectively have a default if: success() condition, meaning they will not run if the workflow is cancelled [1]. By using if: always(), you can override this default behavior to ensure specific tasks continue to run during cancellation [1]. 4. Contextual Notes: - Job Status: The job.status context can have a value of cancelled [2]. - Limitations: In some complex scenarios, such as certain matrix configurations involving reusable workflows, cancelled() might not return true as expected even when the job is cancelled [3]. In such cases, checking the result context of dependencies (e.g., contains(needs.*.result, 'cancelled')) is often used as a workaround [3]. In summary, use if: always() to ensure a step runs despite cancellation, and use if: cancelled() to trigger a step specifically when a cancellation has occurred [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant workflow excerpt =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/test.yml')
if not p.exists():
    print('missing')
    raise SystemExit
for i,line in enumerate(p.read_text().splitlines(), 1):
    if 90 <= i <= 140:
        print(f'{i:6d}: {line}')
PY

echo "== step name references and cancellation guard =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/test.yml')
for i,line in enumerate(p.read_text().splitlines(), 1):
    if any(tok in line for tok in ['update-baselines', 'steps.record.outcome', 'cancelled()', 'record:']):
        print(f'{i}: {line.strip()}')
PY

echo "== check installed GitHub CLI availability/permissions =="
command -v gh || true
if command -v gh >/dev/null 2>&1; then
  env | grep -i gh || true
  gh --version 2>&1 | head -n 5 || true
fi

Repository: jetthoughts/jetthoughts.github.io

Length of output: 4722


🌐 Web query:

GitHub Actions workflow_context cancelled job_status cancelled step outcome cancelled documentation

💡 Result:

In GitHub Actions, the term cancelled is used to describe the status or conclusion of a job or step after a workflow has been manually cancelled or terminated by the system [1][2]. Contexts and Terminology 1. Job Status and Conclusion The status of a job (e.g., job.status) indicates its current lifecycle phase, such as queued, in_progress, or completed [3]. When a job finishes, it receives a conclusion, which describes the final result [3][4]. A job can have a conclusion of cancelled when the workflow run is cancelled before the job completes [3][2]. 2. Step Outcome and Conclusion Each step in a job has an outcome and a conclusion [1][2]: - outcome: The result of the step execution before the continue-on-error setting is applied [1][2]. - conclusion: The final result of the step after the continue-on-error setting is applied [1][2]. If a workflow is cancelled while a step is running, both the outcome and conclusion of that step will typically be marked as cancelled [1][2]. Expression Usage You can use the cancelled expression in if conditional statements to perform specific actions only when a job or step has been cancelled [5][2]. For example: - if: cancelled can be used on jobs or steps to ensure they run even when the workflow has been cancelled [5]. Important Note The term workflow_context is not a standard, top-level context in GitHub Actions expressions; documentation refers to the github context or specific step/job contexts [1][2]. When a workflow is cancelled, the system re-evaluates if conditions, sends termination signals to runners, and eventually forces termination of jobs and steps that do not exit promptly [5].

Citations:


🌐 Web query:

GitHub Actions always cancelled() cancelled step condition example job status cancelled

💡 Result:

In GitHub Actions, the cancelled() status check function is used to run a job or step only if the workflow run has been cancelled [1]. By default, jobs and steps are configured with an implicit success() condition, meaning they are skipped if the workflow is cancelled [1][2]. To ensure a step or job runs even when the workflow is cancelled, you must explicitly use the cancelled() function in your if condition [1][2]. Example: Running a Step on Cancellation You can use if: cancelled() to trigger cleanup or notification steps specifically when a user cancels the workflow [1]. jobs: cleanup-job: runs-on: ubuntu-latest if: cancelled steps: - name: Run on cancellation run: echo "The workflow was cancelled. Performing cleanup." Key Considerations and Limitations: 1. Cancellation Logic: When a workflow is cancelled, GitHub re-evaluates if conditions for all running jobs and steps [2]. Jobs or steps with if: always() or if: cancelled() will continue to execute, while others are marked for cancellation [2]. 2. Implicit Success: If you do not specify an if condition, GitHub defaults to if: success() [1][2]. This is why steps are typically skipped upon cancellation. 3. Job-Level Cancellation: If you use if: always() on a job, it will run regardless of the workflow's status (success, failure, or cancellation) [1]. If you want a job to run on cancellation but NOT on failure, you must explicitly combine conditions, such as if: cancelled() || success() [1]. 4. Composite Actions and Complex Cases: There have been known issues where cancelled() may not behave as expected in certain complex scenarios, such as within specific composite actions or when dependent jobs are skipped [3][4][5]. In some cases, checking contains(needs.*.result, 'cancelled') at the job level provides a more reliable alternative for monitoring the status of upstream dependencies [5]. 5. Termination: After a cancellation is triggered, there is a 5-minute timeout period. If the job or step is still running after this period, the runner will forcibly terminate the process [2][6].

Citations:


🌐 Web query:

site:docs.github.com steps *.outcome cancelled action

💡 Result:

In GitHub Actions, steps.<step_id>.outcome and steps.<step_id>.conclusion are properties within the steps context that indicate the result of a completed step [1]. The outcome property represents the result of a completed step before any continue-on-error setting is applied [1]. Possible values for both outcome and conclusion include success, failure, cancelled, or skipped [1]. The distinction between the two is specifically relevant when continue-on-error is used: - If a step fails and continue-on-error is set to true, the outcome will be failure, but the final conclusion will be success [1]. If a step is cancelled, the outcome will be reported as cancelled [1]. The steps context is only available for steps that have an id specified and have already run [1].

Citations:


Do not commit baselines after cancellation.

Line [124] blocks only steps.record.outcome == 'skipped'. A canned test:system run can leave partial baseline rewrites; the commit step can then push them because always() remains true and steps.record.outcome can be cancelled. Keep the commit for successful and failed recordings, but block it when cancelled.

Guard the commit step against cancellation
-        if: ${{ always() && steps.record.outcome != 'skipped' && github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
+        if: ${{ always() && !cancelled() && steps.record.outcome != 'skipped' && github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# always(): freshly recorded baselines must be committed even when a
# test failed mid-run (a red test does not invalidate the OTHER pages'
# recordings). Gated on the record step actually having run, and the
# commit is a no-op when nothing changed.
- name: Commit updated baselines
if: ${{ github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
if: ${{ always() && steps.record.outcome != 'skipped' && github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
# always(): freshly recorded baselines must be committed even when a
# test failed mid-run (a red test does not invalidate the OTHER pages'
# recordings). Gated on the record step actually having run, and the
# commit is a no-op when nothing changed.
- name: Commit updated baselines
if: ${{ always() && !cancelled() && steps.record.outcome != 'skipped' && github.event_name == 'workflow_dispatch' && inputs.update-baselines }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 119 - 124, Update the “Commit
updated baselines” step condition to allow completed successful or failed
recording steps while explicitly excluding a cancelled steps.record outcome.
Preserve the existing always(), workflow_dispatch, and inputs.update-baselines
guards.

run: |
if [ -z "$(git status --porcelain test/fixtures/screenshots)" ]; then
echo "no baseline changes recorded - nothing to commit"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add test/fixtures/screenshots/
Expand Down
33 changes: 31 additions & 2 deletions .okf/build/ci-gates.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
type: Playbook
title: CI gates (GitHub Actions)
description: PR CI runs Hugo build, unit tests, and a path-scoped broken-internal-link crawl (lychee). Visual regression is not in CI yet - the old cross-OS blocker is gone (pinned glibc stack); re-introduction planned as DevX R2 Phase B.
description: PR CI runs Hugo build, unit tests, a path-scoped broken-internal-link crawl (lychee), and a report-only visual gate on the pinned rendering stack. Sync fan-out is gated - no-op sync runs deploy nothing.
tags: [ci, github-actions, testing, link-check]
resource: .github/workflows/link-check.yml
timestamp: 2026-07-31T00:00:00Z
timestamp: 2026-07-31T16:30:00Z
---

# What CI enforces on a PR
Expand Down Expand Up @@ -53,6 +53,35 @@ Two gotchas the first record run hit (both fixed; evidence: [run 30629929407](ht
- **Draft fixtures**: screenshot tests visit the draft post `/blog/codeblock-styles-fixture/`; local builds pass `--buildDrafts` but `bin/hugo-build` only does so when `BUILD_DRAFTS` is set - test.yml sets `BUILD_DRAFTS: '1'` on its setup-hugo step. A CI test build without it 404s the fixture and fails the codeblock tests on every run.
- **fail_if_new in CI**: snap_diff hard-errors on missing baselines when `ENV["CI"]` is set. Record mode (`FORCE_SCREENSHOT_UPDATE=true`) disables `fail_on_difference` AND `fail_if_new` (setup_snap_diff.rb) so pages added since the last recording can get their FIRST baseline.

# R3-2 correctness + cost fixes (2026-07-31)

- **hugo_stats.json cache is EXACT-key-only** (own cache entry in
setup-hugo, no restore-keys). The warm-up skip trusts any non-empty stats
file; a restore-keys partial hit could restore a STALE stats file from an
older tree and PurgeCSS would purge live classes (the .sr-only incident
class re-opened via cache). Exact hit = identical source tree = valid
stats; any change pays the ~52s warm-up instead. Never add restore-keys
back to that entry.
- **`_dest` is not cached** - it uploaded ~1-2 GB/job and evicted every
other cache from the 10 GB repo quota; the build regenerates it anyway.
- **Sync fan-out gate**: publish.yml + link-check.yml `workflow_run` jobs
run only when `github.event.workflow_run.head_sha != github.sha`
(for workflow_run events github.sha = current default-branch head; the
sync pushing a commit is exactly what moves them apart). "Sync articles"
fires every 10 min 8-21 UTC but usually commits nothing - that was ~84
no-op deploy+test cascades/day.
- **link-check builds ONCE**: setup-hugo runs with `build: 'false'`;
`rake test:links` does its own production build. The double build blew
the job's 10-min timeout on cold caches (seen on PR #422).
- **test.yml checks out the PR MERGE commit** (default checkout) for
pull_request events; only record dispatches check out the branch ref
(needed to push the baseline commit). head_ref checkout tested the tip
without the base merged and breaks fork PRs.
- **Record commits survive red tests**: record runs `rake test:system`
only (a unit failure used to discard 15+ min of recording), and the
commit step is `always()`-gated + porcelain-guarded, so recorded PNGs
land even when one page's test fails.

# libvips gotcha (if a CI job ever needs ruby-vips again)

ruby-vips' `:vips` driver `dlopen`s `libvips.so.42` at runtime, which ships in the **libvips42** runtime package - NOT `libvips-dev` (headers only). Install `libvips42` + run `ldconfig`. snap_diff swallows the real `LoadError` behind generic per-screenshot errors, so verify the load explicitly (`ruby -e 'require "vips"'`).
17 changes: 17 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Bundle Update Log

## 2026-07-31 (R3-2 CI correctness + cost)

* **Update**: [ci-gates](/build/ci-gates.md) - DevX R3-2: hugo_stats.json
moved to its own EXACT-key-only cache (restore-keys could restore stale
stats and re-open the PurgeCSS purge-live-classes incident class via
cache); `_dest` dropped from cache paths (~1-2 GB/job, evicted everything
else); sync fan-out gated (workflow_run jobs skip when the sync pushed
nothing - was ~84 no-op deploy+test cascades/day); link-check builds once
(composite `build: 'false'` - the double build blew its 10-min timeout on
cold caches, seen live on PR #422); test.yml checks out the PR merge
commit (head_ref tested the tip unmerged + broke fork PRs) and records
via `rake test:system` with an always()-gated porcelain-guarded commit
step (a red unit test used to discard 15+ min of recorded baselines).
Also: baseline re-record landed (2edb94d9) on the pinned stack - third
attempt, after #417 (drafts/fail_if_new) and #418 (push race + swallowed
retry failure).

## 2026-07-31 (R3-1 harness truth)

* **Update**: [test-gates](/build/test-gates.md) - all four test runners now
Expand Down
Loading