Skip to content

Commit 4d80461

Browse files
author
merge-queue-bot
committed
Merge PR #481: Fix Pages deploy: repoint link probes off pruned docs + verify render in PR CI
2 parents 3ed6c86 + a2bedbd commit 4d80461

4 files changed

Lines changed: 87 additions & 57 deletions

File tree

.github/workflows/pages.yml

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ name: Deploy mdsmith.dev
1111
# site is pure HTML built from docs/**/*.md and the homepage data
1212
# files, so it never needs the registry-publish credentials.
1313
#
14-
# The `build-content` job (synced-tree lint) runs on push to main
15-
# AND on every pull_request that touches the same paths, so a
16-
# broken-link regression fails the PR rather than only surfacing
17-
# after merge. The `deploy` job is gated to main, so PR runs stop
18-
# at the lint and never publish.
14+
# The `build-content` job runs on push to main AND on every
15+
# pull_request that touches the same paths. It lints the synced
16+
# tree, then renders the site with Hugo and runs the rendered-HTML
17+
# probes (verify-website-links), so a broken-link OR render-link
18+
# regression fails the PR rather than only surfacing after merge.
19+
# The `deploy` job is gated to main, so PR runs stop before publish.
1920
on:
2021
push:
2122
branches: [main]
@@ -75,13 +76,14 @@ jobs:
7576
uses: ./.github/workflows/mdsmith-check.yml
7677

7778
build-content:
78-
# The synced-tree lint: build the Hugo content tree from
79-
# docs/, then run `mdsmith check` against it with the
80-
# build-output config (link integrity on, style rules off).
81-
# Catches broken-link regressions on PR rather than only on
82-
# the post-merge deploy. deploy `needs: build-content`, so
83-
# push / workflow_dispatch / workflow_call runs all gate on
84-
# this lint before publishing.
79+
# Build the Hugo content tree from docs/, run `mdsmith check`
80+
# against it with the build-output config (link integrity on,
81+
# style rules off), then render the site with Hugo and probe the
82+
# rendered HTML (verify-website-links). Catches both broken-link
83+
# and render-link regressions on PR rather than only on the
84+
# post-merge deploy. deploy `needs: build-content`, so push /
85+
# workflow_dispatch / workflow_call runs all gate on this job
86+
# before publishing.
8587
#
8688
# deploy re-runs the same sync and lint after the
8789
# version-stamp + site-assets pulls (which mutate docs/
@@ -96,6 +98,10 @@ jobs:
9698
runs-on: ubuntu-latest
9799
permissions:
98100
contents: read
101+
env:
102+
# Render the site with the same Hugo the deploy uses so the
103+
# rendered-HTML probes below run on PR, not only post-merge.
104+
HUGO_VERSION: "0.161.1"
99105
steps:
100106
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
101107
with:
@@ -125,6 +131,45 @@ jobs:
125131
--config ./website/build-output.mdsmith.yml \
126132
--no-gitignore \
127133
./website/content/docs
134+
- name: Install Hugo
135+
# `go install` resolves via the Go module proxy with sumdb
136+
# checksum verification, so the binary is pinned by content
137+
# hash even though the version selector is a tag.
138+
run: go install github.com/gohugoio/hugo@v${HUGO_VERSION}
139+
- name: Render site
140+
# Render the synced tree so the steps below can probe the
141+
# real rendered HTML. The synced-tree lint above operates on
142+
# the markdown filesystem (pre-render) and cannot see the
143+
# render-link hook output, which is exactly what broke the
144+
# deploy after the maintainer-doc prune: a probe pinned to a
145+
# now-unpublished page. Root baseURL — the production deploy
146+
# is served from the domain root, so relURL emits root paths.
147+
working-directory: website
148+
env:
149+
HUGO_ENVIRONMENT: production
150+
run: hugo --minify --baseURL "/"
151+
- name: Verify render-link hook output
152+
# Probe the rendered HTML for the render-link behaviors
153+
# (.md → permalink, site-absolute /rules/ hrefs, no leaked
154+
# README.md / javascript: / data: targets). The probes live
155+
# in internal/release/verifylinks.go with unit-test coverage.
156+
# The deploy job runs the same check, but only on main; this
157+
# PR-time copy fails a regression before merge.
158+
run: |
159+
go run ./cmd/mdsmith-release verify-website-links \
160+
--dir ./website/public --base-url ""
161+
- name: Verify baseURL prefix on absolute hrefs
162+
# Re-render with a fake subpath baseURL and re-probe, so a
163+
# render-link regression in the relURL prefixing (which a
164+
# root-baseURL render cannot exercise) also fails the PR.
165+
working-directory: website
166+
run: |
167+
hugo --minify --baseURL "https://example.com/mdsmith/" \
168+
--destination /tmp/baseurl-probe
169+
cd ..
170+
go run ./cmd/mdsmith-release verify-website-links \
171+
--dir /tmp/baseurl-probe \
172+
--base-url "https://example.com/mdsmith/"
128173
129174
deploy:
130175
name: Deploy mdsmith.dev to GitHub Pages

cmd/mdsmith-release/main_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,17 +298,14 @@ func TestRunBuildWebsiteEndToEnd(t *testing.T) {
298298
// exercised end-to-end.
299299
func TestRunVerifyWebsiteLinksHappyPath(t *testing.T) {
300300
root := t.TempDir()
301-
mq := filepath.Join(root, "development", "merge-queue", "index.html")
302-
aa := filepath.Join(root, "development", "architecture-audit", "index.html")
301+
ref := filepath.Join(root, "reference", "index.html")
303302
st := filepath.Join(root, "reference", "schema-types", "index.html")
304303
rule := filepath.Join(root, "rules", "mds001", "index.html")
305-
for _, dir := range []string{filepath.Dir(mq), filepath.Dir(aa), filepath.Dir(st), filepath.Dir(rule)} {
304+
for _, dir := range []string{filepath.Dir(ref), filepath.Dir(st), filepath.Dir(rule)} {
306305
require.NoError(t, os.MkdirAll(dir, 0o755))
307306
}
308-
require.NoError(t, os.WriteFile(mq,
309-
[]byte(`<a href="/development/pr-fixup-workflow/">x</a>`), 0o644))
310-
require.NoError(t, os.WriteFile(aa,
311-
[]byte(`<a href="/development/architecture/">x</a>`), 0o644))
307+
require.NoError(t, os.WriteFile(ref,
308+
[]byte(`<a href="/reference/cli/">x</a>`), 0o644))
312309
require.NoError(t, os.WriteFile(st,
313310
[]byte(`<a href="/rules/mds020-required-structure/">x</a>`), 0o644))
314311
require.NoError(t, os.WriteFile(rule,

internal/release/verifylinks.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,17 @@ func websiteLinkProbes(prefix string) []linkProbe {
8080
hrefEq := `href="?` // allow both quoted (default) and unquoted (minified) emission
8181
return []linkProbe{
8282
{
83+
// A `.md` content link must render to the target
84+
// page's clean permalink (trailing slash, no `.md`).
85+
// reference/index.md links its sibling `cli.md`, which
86+
// Hugo serves at /reference/cli/. Pinned to a stable
87+
// user-facing page; the previous form pointed at a
88+
// docs/development/ page that the maintainer-doc prune
89+
// removed from the published site.
8390
name: "sibling .md resolves to target permalink",
84-
path: "development/merge-queue/index.html",
91+
path: "reference/index.html",
8592
wantMatch: regexp.MustCompile(
86-
hrefEq + q(prefix) + `/development/pr-fixup-workflow/`),
87-
},
88-
{
89-
name: "index.md drop resolves to section URL on leaf page",
90-
path: "development/architecture-audit/index.html",
91-
wantMatch: regexp.MustCompile(
92-
hrefEq + q(prefix) + `/development/architecture/`),
93+
hrefEq + q(prefix) + `/reference/cli/`),
9394
},
9495
{
9596
// The rewriter emits site-absolute `/rules/<id>/`

internal/release/verifylinks_test.go

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import (
1919
func goodSite(t *testing.T, prefix string) string {
2020
t.Helper()
2121
root := t.TempDir()
22-
writeFile(t, filepath.Join(root, "development", "merge-queue", "index.html"),
23-
`<a href="`+prefix+`/development/pr-fixup-workflow/">pr fixup</a>`)
24-
writeFile(t, filepath.Join(root, "development", "architecture-audit", "index.html"),
25-
`<a href="`+prefix+`/development/architecture/">arch</a>`)
22+
// A .md content link rendered to a clean doc permalink
23+
// (reference/index.md links its sibling cli.md, served at
24+
// /reference/cli/) — satisfies the sibling-.md probe.
25+
writeFile(t, filepath.Join(root, "reference", "index.html"),
26+
`<a href="`+prefix+`/reference/cli/">cli</a>`)
2627
writeFile(t, filepath.Join(root, "reference", "schema-types", "index.html"),
2728
`<a href="`+prefix+`/rules/mds020-required-structure/">rule</a>`)
2829
writeFile(t, filepath.Join(root, "rules", "mds001", "index.html"),
@@ -48,37 +49,27 @@ func TestVerifyWebsiteLinks_SubpathDeployPasses(t *testing.T) {
4849
// match `href=value` as well as `href="value"`.
4950
func TestVerifyWebsiteLinks_AcceptsUnquotedHref(t *testing.T) {
5051
root := t.TempDir()
51-
writeFile(t, filepath.Join(root, "development", "merge-queue", "index.html"),
52-
`<a href=/development/pr-fixup-workflow/>pr fixup</a>`)
53-
writeFile(t, filepath.Join(root, "development", "architecture-audit", "index.html"),
54-
`<a href=/development/architecture/>arch</a>`)
52+
writeFile(t, filepath.Join(root, "reference", "index.html"),
53+
`<a href=/reference/cli/>cli</a>`)
5554
writeFile(t, filepath.Join(root, "reference", "schema-types", "index.html"),
5655
`<a href=/rules/mds020-required-structure/>rule</a>`)
5756
writeFile(t, filepath.Join(root, "rules", "mds001", "index.html"),
5857
`<a href=/rules/mds021/>sibling</a>`)
5958
require.NoError(t, VerifyWebsiteLinks(root, ""))
6059
}
6160

61+
// TestVerifyWebsiteLinks_FailsOnMissingSiblingMD removes the only
62+
// clean doc permalink so the recursive sibling-.md probe finds no
63+
// match anywhere in the rendered tree.
6264
func TestVerifyWebsiteLinks_FailsOnMissingSiblingMD(t *testing.T) {
6365
root := goodSite(t, "")
64-
writeFile(t, filepath.Join(root, "development", "merge-queue", "index.html"),
65-
`<a href="pr-fixup-workflow.md">stale .md ref</a>`)
66+
writeFile(t, filepath.Join(root, "reference", "index.html"),
67+
`<a href="cli.md">stale .md ref</a>`)
6668
err := VerifyWebsiteLinks(root, "")
6769
require.Error(t, err)
6870
assert.Contains(t, err.Error(), "sibling .md")
6971
}
7072

71-
func TestVerifyWebsiteLinks_FailsOnIndexMDMisresolved(t *testing.T) {
72-
root := goodSite(t, "")
73-
// Simulate the bug PR #309 fixed: relative target stayed
74-
// relative, browser resolves below the leaf page.
75-
writeFile(t, filepath.Join(root, "development", "architecture-audit", "index.html"),
76-
`<a href="architecture/">stale relative</a>`)
77-
err := VerifyWebsiteLinks(root, "")
78-
require.Error(t, err)
79-
assert.Contains(t, err.Error(), "index.md drop")
80-
}
81-
8273
func TestVerifyWebsiteLinks_FailsOnLeakedREADMEHref(t *testing.T) {
8374
root := goodSite(t, "")
8475
writeFile(t, filepath.Join(root, "rules", "mds999", "index.html"),
@@ -131,10 +122,8 @@ func TestVerifyWebsiteLinks_FailsOnMissingSiteAbsolute(t *testing.T) {
131122
// Build a tree that has every required href except the
132123
// site-absolute /rules/mdsxxx/ form.
133124
root := t.TempDir()
134-
writeFile(t, filepath.Join(root, "development", "merge-queue", "index.html"),
135-
`<a href="/mdsmith/development/pr-fixup-workflow/">x</a>`)
136-
writeFile(t, filepath.Join(root, "development", "architecture-audit", "index.html"),
137-
`<a href="/mdsmith/development/architecture/">x</a>`)
125+
writeFile(t, filepath.Join(root, "reference", "index.html"),
126+
`<a href="/mdsmith/reference/cli/">x</a>`)
138127
// No MDS-rule href under any subpath.
139128
err := VerifyWebsiteLinks(root, "https://example.com/mdsmith/")
140129
require.Error(t, err)
@@ -180,13 +169,11 @@ func TestVerifyWebsiteLinks_InvalidBaseURLWraps(t *testing.T) {
180169
// calls the callback with a stat error.
181170
func TestVerifyWebsiteLinks_MissingRecursiveRootWraps(t *testing.T) {
182171
root := t.TempDir()
183-
// Materialize only the non-recursive probe targets plus a
172+
// Materialize only the non-recursive probe target plus a
184173
// page carrying the site-absolute rule href, so we reach
185174
// the recursive `no README.md leak` probe.
186-
writeFile(t, filepath.Join(root, "development", "merge-queue", "index.html"),
187-
`<a href="/development/pr-fixup-workflow/">x</a>`)
188-
writeFile(t, filepath.Join(root, "development", "architecture-audit", "index.html"),
189-
`<a href="/development/architecture/">x</a>`)
175+
writeFile(t, filepath.Join(root, "reference", "index.html"),
176+
`<a href="/reference/cli/">x</a>`)
190177
writeFile(t, filepath.Join(root, "reference", "schema-types", "index.html"),
191178
`<a href="/rules/mds020-required-structure/">x</a>`)
192179
// rules/ is absent.

0 commit comments

Comments
 (0)