Skip to content

Commit e4cad3c

Browse files
jstirnamanclaude
andauthored
test: add render-regression checks to prevent #7079-class bugs (#7085)
Adds four layers of coverage against the class of rendering bug reported in #7079: whitespace leaks in Hugo render hooks or wrapper shortcode templates that cause Goldmark to HTML-escape highlighted code blocks. Each layer catches the bug at a different point in the lifecycle — together they mean this specific regression can't land again without somebody actively defeating every layer. Layer 1 — Site-wide HTML grep after Hugo build .ci/scripts/check-render-artifacts.sh Scans every .html file under `public/` for three literal patterns that can only appear when a render hook leaked whitespace into its output: `&lt;div class=&quot;highlight&quot;`, `&lt;pre tabindex=&quot;0&quot;`, and `&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;cl&quot;`. Runs in ~1 second on the full built site. Catches regressions on every page automatically, including pages no test lists explicitly. Verified against a rebuild with the pre-fix render-codeblock.html from commit d80eb2f — correctly reports 54 affected files — and against the current clean build — reports 0. Layer 2 — Pre-commit lint for render hooks .ci/scripts/check-render-hook-whitespace.sh lefthook.yml: new pre-commit command `check-render-hook-whitespace` Enforces the invariant that every action tag inside `layouts/_default/_markup/render-*.html` uses `{{- ... -}}` whitespace trimming. Bare `{{ ... }}` actions leak their surrounding indent and newline into the render-hook output; the lint catches this at commit time, before a PR is even opened. Scoped narrowly to render hooks so it doesn't nag on normal Hugo templates where bare actions are fine. Also hardens render-codeblock.html lines 1 and 19, which had bare `{{ ... }}` actions that happened to work correctly because surrounding `{{- ... -}}` operators trimmed their whitespace by accident. The lint flagged them as fragile; rewriting them as `{{- ... -}}` makes the file unconditionally correct and lets the lint ship with no exceptions. Layer 3 — Cypress render-regression spec cypress/e2e/content/render-regression.cy.js package.json: `test:render-regression`, `test:render-artifacts` DOM-level assertion that no `pre > code` element on a curated page list contains the escaped chroma fragments. Two sections: * Shortcode examples page (/example/) — a single exhaustive shortcode fixture that covers every render-hook combination documented in the site. * Representative product pages — one page per InfluxDB 3 edition, hand-picked for the wrapper/attribute combination it exercises: - core/reference/sample-data (placeholders + custom-timestamps + code-tab-content) - enterprise/admin/backup-restore (placeholders inside tab-content — the page that regressed worst in #7079) - cloud-dedicated/reference/sample-data (placeholders with regex grouping inside custom-timestamps) - clustered/admin/users/add (placeholders + callouts in nested expand wrappers) - cloud-serverless/reference/sample-data (post-migration fence-attribute syntax) Layer 4 — Example page coverage for the render-hook attributes content/example.md: new "Render-regression fixtures" section Adds fence-attribute and wrapper fixtures that exercise every code path implicated in #7079: `placeholders=` with and without regex grouping, `callout=` with default and explicit color, combined `placeholders=` + `callout=`, placeholder fences inside `influxdb/custom-timestamps`, and the worst-case nested shape (expand > code-tabs > code-tab-content > custom-timestamps > placeholder fence) that every sample-data page uses. Before this commit `content/example.md` had zero uses of either attribute, which meant the shortcode-examples smoke test was blind to #7079-class bugs. Verified after rebuild: 11 placeholder wrappers, 3 callouts, and 3 custom-timestamps wrappers render on /example/. CI workflow .github/workflows/pr-render-check.yml Runs on every pull_request. Two jobs: * check-artifacts: Unconditional site-wide grep on every PR. * cypress-render: Cypress spec, gated on a path filter that only triggers when the PR touches layouts/, assets/, the render-regression spec itself, the check-artifacts script, or this workflow file. The unconditional grep is the backstop — it runs even on content-only PRs and catches any regression anywhere in the site. Cypress runs on the subset of PRs that can actually cause this class of bug. Refs #7079 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1c38888 commit e4cad3c

File tree

8 files changed

+527
-2
lines changed

8 files changed

+527
-2
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bash
2+
#
3+
# check-render-artifacts.sh
4+
#
5+
# Scan built Hugo HTML for forbidden render artifacts — patterns that
6+
# should be impossible on a correctly built page. Their presence signals
7+
# a rendering bug (whitespace leak in a render hook, broken shortcode
8+
# template, etc.) that would otherwise ship to production as visibly
9+
# broken content.
10+
#
11+
# The canonical example is influxdata/docs-v2#7079, where a whitespace
12+
# leak in the `placeholders`/`callout` branch of render-codeblock.html
13+
# caused Goldmark to HTML-escape highlighted code blocks into literal
14+
# `<pre><code>&lt;div class=&quot;highlight&quot;…` fragments on every
15+
# page that used either attribute. Every pattern in this script is a
16+
# fingerprint of that class of bug.
17+
#
18+
# Usage:
19+
# .ci/scripts/check-render-artifacts.sh [target]
20+
#
21+
# Arguments:
22+
# target Directory or file to scan. Defaults to `public`.
23+
#
24+
# Exit codes:
25+
# 0 No forbidden patterns found.
26+
# 1 At least one forbidden pattern found. The script prints every
27+
# offending file and the pattern that matched.
28+
# 2 Target directory/file does not exist.
29+
#
30+
# Run this immediately after `npx hugo --quiet` in CI so that any
31+
# regression fails the build before downstream jobs (Cypress, Vale,
32+
# link-check) waste time.
33+
#
34+
# Known gaps / possible future additions:
35+
#
36+
# - `{{<` / `{{%` : Indicates an unrendered Hugo shortcode. Currently
37+
# excluded because legitimate docs contain Helm and
38+
# Go template examples that use `{{` syntax inside
39+
# code fences, producing unavoidable false positives.
40+
# Could be re-added as a scoped regex that requires
41+
# the delimiters to appear outside `<code>` / `<pre>`.
42+
#
43+
# - `ZgotmplZ` : Hugo's context-escape sentinel. Currently excluded
44+
# because a pre-existing bug emits it into
45+
# `data-influxdb-urls="#ZgotmplZ"` on ~4600 pages.
46+
# Re-add after that bug is fixed.
47+
48+
set -euo pipefail
49+
50+
TARGET="${1:-public}"
51+
52+
if [[ ! -e "$TARGET" ]]; then
53+
echo "::error::check-render-artifacts: target '$TARGET' does not exist. Did Hugo build succeed?"
54+
exit 2
55+
fi
56+
57+
# Forbidden patterns, format: "PATTERN|DESCRIPTION"
58+
#
59+
# Every pattern below is HTML-escaped chroma output (`<div class="highlight">`,
60+
# `<pre tabindex="0">`, `<span class="line">`). Goldmark only produces those
61+
# escaped forms when it has interpreted legitimate chroma HTML as plain-text
62+
# content — i.e. when a render hook or wrapper shortcode leaked whitespace and
63+
# Goldmark re-wrapped the highlighted output as an indented code block.
64+
PATTERNS=(
65+
"&lt;div class=&quot;highlight&quot;|Goldmark re-wrapped highlighted code as an indented code block (see #7079). Likely cause: whitespace leak in a render hook (layouts/_default/_markup/render-*.html) or a wrapper shortcode template."
66+
"&lt;pre tabindex=&quot;0&quot;|Escaped chroma <pre> wrapper — same class of bug as #7079."
67+
"&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;cl&quot;|Escaped chroma line span — same class of bug as #7079."
68+
)
69+
70+
failed=0
71+
total_hits=0
72+
73+
for entry in "${PATTERNS[@]}"; do
74+
pattern="${entry%%|*}"
75+
description="${entry#*|}"
76+
77+
if [[ -d "$TARGET" ]]; then
78+
hits=$(grep -rlF --include='*.html' "$pattern" "$TARGET" 2>/dev/null || true)
79+
else
80+
hits=$(grep -lF "$pattern" "$TARGET" 2>/dev/null || true)
81+
fi
82+
83+
if [[ -n "$hits" ]]; then
84+
count=$(printf '%s\n' "$hits" | wc -l | tr -d ' ')
85+
total_hits=$((total_hits + count))
86+
failed=1
87+
88+
echo "::error::Found forbidden render artifact in $count file(s): '$pattern'"
89+
echo " Cause: $description"
90+
echo " First 10 affected files:"
91+
printf '%s\n' "$hits" | head -10 | sed 's/^/ /'
92+
echo ""
93+
fi
94+
done
95+
96+
if [[ $failed -eq 0 ]]; then
97+
echo "✅ check-render-artifacts: no forbidden patterns found in '$TARGET'."
98+
exit 0
99+
fi
100+
101+
echo "❌ check-render-artifacts: found $total_hits rendering artifact(s) across the built site."
102+
echo ""
103+
echo "These signatures appear only when a render hook or wrapper shortcode leaks"
104+
echo "whitespace into its output and Goldmark re-wraps highlighted code as an"
105+
echo "indented code block. The fix is almost always to add {{- ... -}} whitespace"
106+
echo "trimming to every action tag in the offending template. See"
107+
echo "influxdata/docs-v2#7079 for the canonical case and fix pattern."
108+
exit 1
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
#
3+
# check-render-hook-whitespace.sh
4+
#
5+
# Pre-commit lint for Hugo render hooks (layouts/_default/_markup/render-*.html).
6+
#
7+
# Enforces the invariant that every action tag inside a render hook must
8+
# use `{{- ... -}}` whitespace trimming. Bare `{{ ... }}` actions leak
9+
# their surrounding indent and trailing newline into the rendered output,
10+
# which causes Goldmark to interpret the result as an indented code block
11+
# and HTML-escape any leading HTML (see influxdata/docs-v2#7079 for the
12+
# canonical failure mode).
13+
#
14+
# The only exception is template comments (`{{/* ... */}}`), which
15+
# produce no output regardless of trimming.
16+
#
17+
# Usage:
18+
# .ci/scripts/check-render-hook-whitespace.sh [file...]
19+
#
20+
# Typical invocation from lefthook pre-commit:
21+
# glob: "layouts/_default/_markup/render-*.html"
22+
# run: .ci/scripts/check-render-hook-whitespace.sh {staged_files}
23+
#
24+
# Exit codes:
25+
# 0 All action tags in the provided files are whitespace-trimmed.
26+
# 1 At least one bare `{{ ... }}` action was found.
27+
28+
set -euo pipefail
29+
30+
if [[ $# -eq 0 ]]; then
31+
exit 0
32+
fi
33+
34+
failed=0
35+
36+
for file in "$@"; do
37+
[[ -f "$file" ]] || continue
38+
39+
# Match any line whose first non-whitespace `{{` is not followed by `-`
40+
# (i.e. a bare opening action), AND whose matching `}}` is not preceded
41+
# by `-` (bare closing action).
42+
#
43+
# Exclude template comments `{{/* ... */}}` since they produce no
44+
# output and are unaffected by whitespace.
45+
#
46+
# Exclude lines where `{{` appears only inside a string literal (e.g.
47+
# `print "...{{..."`), detected by requiring the action to start the
48+
# line (after optional whitespace).
49+
offenders=$(grep -nE '^\s*\{\{[^-/]' "$file" | grep -vE '\{\{/\*' || true)
50+
51+
# Also catch bare closing actions: `... }}` without the leading `-}}`.
52+
# Only warn on lines that also start with an action (avoid false
53+
# positives from string literals spanning multiple lines).
54+
closing_offenders=$(grep -nE '[^-]\}\}\s*$' "$file" | grep -E '^\s*\{\{' | grep -vE '\{\{-' || true)
55+
56+
all_offenders=$(printf '%s\n%s\n' "$offenders" "$closing_offenders" | grep -v '^$' | sort -u || true)
57+
58+
if [[ -n "$all_offenders" ]]; then
59+
failed=1
60+
echo "$file: found bare {{ ... }} action tag(s) in a render hook."
61+
echo " Every action inside layouts/_default/_markup/render-*.html must"
62+
echo " use whitespace-trimming delimiters ({{- ... -}}) to prevent"
63+
echo " leaked whitespace from breaking Goldmark's HTML-block detection."
64+
echo " See influxdata/docs-v2#7079 for the canonical failure."
65+
echo ""
66+
printf '%s\n' "$all_offenders" | sed 's/^/ /'
67+
echo ""
68+
fi
69+
done
70+
71+
if [[ $failed -eq 1 ]]; then
72+
echo "Fix: rewrite each offending action with {{- ... -}} trimming."
73+
echo "Example: '{{ \$x := foo }}' → '{{- \$x := foo -}}'"
74+
exit 1
75+
fi
76+
77+
exit 0
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Render Regression Check
2+
3+
# Guards against the class of rendering bug reported in
4+
# influxdata/docs-v2#7079: whitespace leaks in Hugo render hooks or
5+
# wrapper shortcode templates that cause Goldmark to HTML-escape
6+
# highlighted code blocks on every affected page.
7+
#
8+
# Two layers:
9+
#
10+
# 1. check-artifacts: Site-wide grep of built HTML. Cheap, runs on
11+
# every PR, catches regressions anywhere in the site even on pages
12+
# nobody is thinking about. This is the backstop.
13+
#
14+
# 2. cypress-render: Cypress spec against content/example.md and a
15+
# curated set of representative product pages. Runs only when a
16+
# PR touches `layouts/` or `assets/` (i.e. the code paths that can
17+
# cause this class of bug). Provides DOM-level verification that
18+
# the pages actually look right, not just that they lack a string.
19+
20+
on:
21+
pull_request:
22+
types: [opened, synchronize, reopened]
23+
24+
jobs:
25+
check-artifacts:
26+
name: Scan built HTML for forbidden render artifacts
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v6
31+
32+
- name: Setup Node.js
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: '20'
36+
cache: 'yarn'
37+
38+
- name: Install dependencies
39+
env:
40+
CYPRESS_INSTALL_BINARY: 0
41+
run: yarn install --frozen-lockfile
42+
43+
- name: Build Hugo site
44+
run: npx hugo --quiet
45+
46+
- name: Scan built HTML for render artifacts
47+
run: .ci/scripts/check-render-artifacts.sh public
48+
49+
cypress-render:
50+
name: Cypress render-regression spec
51+
runs-on: ubuntu-latest
52+
# Only run when a PR touches code that could regress rendering.
53+
# Content-only PRs don't need this layer — the site-wide grep above
54+
# already catches any page-level impact they could have.
55+
if: |
56+
contains(github.event.pull_request.labels.*.name, 'render-regression') ||
57+
github.event.pull_request.draft == false
58+
steps:
59+
- name: Checkout repository
60+
uses: actions/checkout@v6
61+
with:
62+
fetch-depth: 0
63+
64+
- name: Detect layout/asset changes
65+
id: detect
66+
run: |
67+
# Only run Cypress when files under layouts/, assets/, or
68+
# the render-regression spec itself have changed. Content-only
69+
# PRs rely on the site-wide grep job above for coverage.
70+
CHANGED=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
71+
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \
72+
| jq -r '.[].filename')
73+
74+
echo "Changed files:"
75+
echo "$CHANGED"
76+
77+
if echo "$CHANGED" | grep -qE '^(layouts/|assets/|cypress/e2e/content/render-regression\.cy\.js$|cypress/support/|\.ci/scripts/check-render-artifacts\.sh$|\.github/workflows/pr-render-check\.yml$)'; then
78+
echo "should-run=true" >> $GITHUB_OUTPUT
79+
echo "✅ Layout, asset, or render-check file changed — running Cypress"
80+
else
81+
echo "should-run=false" >> $GITHUB_OUTPUT
82+
echo "ℹ️ No layout/asset/render-check file changed — skipping Cypress"
83+
fi
84+
85+
- name: Setup Node.js
86+
if: steps.detect.outputs.should-run == 'true'
87+
uses: actions/setup-node@v4
88+
with:
89+
node-version: '20'
90+
cache: 'yarn'
91+
92+
- name: Install dependencies
93+
if: steps.detect.outputs.should-run == 'true'
94+
run: yarn install --frozen-lockfile
95+
96+
- name: Run render-regression spec
97+
if: steps.detect.outputs.should-run == 'true'
98+
run: |
99+
node cypress/support/run-e2e-specs.js \
100+
--spec "cypress/e2e/content/render-regression.cy.js" \
101+
--no-mapping

content/example.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,3 +1471,108 @@ and all the rows with the `hum` field will be in another.
14711471
## Ask AI Link
14721472

14731473
Can't access your InfluxDB instance? {{< ask-ai-link link-text="Ask InfluxData AI" query="What's my InfluxDB version?" >}} for help.
1474+
1475+
## Render-regression fixtures
1476+
1477+
The sections below exercise the code-block render hook combinations that
1478+
were broken in influxdata/docs-v2#7079. They exist to give the
1479+
`cypress/e2e/content/render-regression.cy.js` spec something to assert
1480+
against and to make this page a meaningful smoke test for any PR that
1481+
touches `layouts/_default/_markup/render-codeblock.html` or any wrapper
1482+
shortcode that contains fenced code. When adding coverage for a new
1483+
render-hook attribute combination, add a matching fixture here.
1484+
1485+
### Placeholder fence attribute
1486+
1487+
```sh { placeholders="DATABASE_NAME|AUTH_TOKEN" }
1488+
curl --request POST \
1489+
http://localhost:8181/api/v3/write_lp?db=DATABASE_NAME \
1490+
--header "Authorization: Bearer AUTH_TOKEN" \
1491+
--data-raw "home,room=Kitchen temp=22.1"
1492+
```
1493+
1494+
### Placeholder fence attribute with regex group
1495+
1496+
```sh { placeholders="DATABASE_(TOKEN|NAME)" }
1497+
influxctl write --token DATABASE_TOKEN --database DATABASE_NAME
1498+
```
1499+
1500+
### Callout fence attribute
1501+
1502+
```sh { callout="--host" }
1503+
influx query --host http://localhost:8086
1504+
```
1505+
1506+
### Callout fence attribute with explicit color
1507+
1508+
```sh { callout="--host" callout-color="magenta" }
1509+
influx query --host http://localhost:8086
1510+
```
1511+
1512+
### Placeholder and callout on the same fence
1513+
1514+
```sh { placeholders="DATABASE_NAME" callout="--host" callout-color="orange" }
1515+
influx query --host http://localhost:8086 --database DATABASE_NAME
1516+
```
1517+
1518+
### Placeholder fence inside a custom-timestamps wrapper
1519+
1520+
The `influxdb/custom-timestamps` wrapper was one of the shortcodes
1521+
affected by #7079 — it wraps code blocks that include timestamps so the
1522+
UI can rewrite them. This fixture exercises a placeholder code fence
1523+
inside that wrapper.
1524+
1525+
{{% influxdb/custom-timestamps %}}
1526+
1527+
```sh { placeholders="DATABASE_NAME|AUTH_TOKEN" }
1528+
influxdb3 write --token AUTH_TOKEN --database DATABASE_NAME \
1529+
'home,room=Kitchen temp=22.1 1641024000'
1530+
```
1531+
1532+
{{% /influxdb/custom-timestamps %}}
1533+
1534+
### Placeholder fence inside code-tab-content and custom-timestamps
1535+
1536+
The worst-case path: placeholders inside a code-tab-content section
1537+
inside a code-tabs wrapper inside an expand wrapper. This is the exact
1538+
shape that sample-data pages use.
1539+
1540+
{{< expand-wrapper >}}
1541+
{{% expand "Expand to see the tabbed, placeholder-enabled code block" %}}
1542+
1543+
{{< code-tabs-wrapper >}}
1544+
{{% code-tabs %}}
1545+
[influxdb3](#)
1546+
[v2 API](#)
1547+
{{% /code-tabs %}}
1548+
{{% code-tab-content %}}
1549+
1550+
{{% influxdb/custom-timestamps %}}
1551+
1552+
```sh { placeholders="DATABASE_NAME|AUTH_TOKEN" }
1553+
influxdb3 write --token AUTH_TOKEN --database DATABASE_NAME \
1554+
'home,room=Kitchen temp=22.1 1641024000'
1555+
```
1556+
1557+
{{% /influxdb/custom-timestamps %}}
1558+
1559+
{{% /code-tab-content %}}
1560+
{{% code-tab-content %}}
1561+
1562+
{{% influxdb/custom-timestamps %}}
1563+
1564+
```sh { placeholders="DATABASE_NAME|AUTH_TOKEN" }
1565+
curl --request POST \
1566+
http://localhost:8086/api/v2/write?bucket=DATABASE_NAME \
1567+
--header "Authorization: Bearer AUTH_TOKEN" \
1568+
--data-binary "home,room=Kitchen temp=22.1 1641024000"
1569+
```
1570+
1571+
{{% /influxdb/custom-timestamps %}}
1572+
1573+
{{% /code-tab-content %}}
1574+
{{< /code-tabs-wrapper >}}
1575+
1576+
{{% /expand %}}
1577+
{{< /expand-wrapper >}}
1578+

0 commit comments

Comments
 (0)