feat: harden OINK platform and version delivery - #472
Conversation
- reject the repository root and its ancestors as outputs - reject every output path nested inside the active checkout - cover root, parent, and checkout-child deletion attempts
- add five-group docs navigation and explicit sidebar recovery\n- isolate and persist sidebar state by version and locale\n- add click-gated Kapa adapter with strict privacy defaults\n- enable OINK content helpers and branded social fallback\n- cover UI and AI contracts with dependency-free Node tests
- preserve mobile safe-area positioning\n- defer max and env evaluation to browser CSS\n- keep Hugo Sass compilation compatible
- preserve safe-area browser functions through Sass\n- replace unsupported color syntax with rgba values\n- keep responsive status and menu styles buildable
- compile safe-area and translucent styles with Hugo Sass\n- expose sidebar restore only while the tree is collapsed\n- scope backlinks and image zoom to designed surfaces\n- add enabled and invalid AI build fixtures\n- verify CSP, social image, and feature-scope contracts
- add immutable 1.3 and 1.0 refs to the canonical manifest - migrate legacy routes into the five-group information architecture - enforce real hreflang, archive noindex, and sitemap exclusion - support latest-only aggregates and cross-origin staging menus
- enumerate every registered checkout before deleting outputs - reject git-marked candidate paths and ancestors - fail closed when worktree discovery is unavailable - cover sibling and discovery-failure regressions
- enable LLMSFULL only on the English Docs root\n- enable LLMSFULL only on the Chinese Docs root\n- keep other sections and historical content unchanged
- map PR, master, and trusted dispatch events to fixed targets - derive direct Hugo version menus from versions.json - isolate Hugo caches and provision Community render dependencies - separate read-only deploy gates from write-enabled publishing
- add a locked Node 24 Playwright workspace - verify five-version archive and alias contracts in Chromium - assert fixed bilingual Lunr queries rank expected references - keep visual evidence advisory and outside the deploy gate
- generate locale-aware aliases after historical builds - mark every archived page noindex,follow - validate archive robots metadata in artifacts - upgrade Playwright past the browser-download advisory
- fetch every resolved build SHA inside its isolated job - build an AI-enabled fixture from the manifest-derived config - gate shell, AI, Community, ranking, and axe contracts - keep visual captures advisory with the correct theme key
- remove duplicated static version arrays from Hugo config - keep localized menu labels in the stable site config - require generated version config for release entries
- force noindex,follow on regular archive and print pages - retain noindex,nofollow on generated 404 documents - cover both prior robots states in the regression fixture
- add noindex metadata when Hugo alias pages omit robots - preserve dedicated nofollow handling for 404 documents - fail closed when archive HTML has no head or robots marker
- fetch the generated redirect document without browser navigation - assert raw refresh and canonical targets - avoid racing automatic meta refresh in Chromium
- load the metadata query fixture when PR-B is integrated - keep PR-A fallback cases for standalone validation - assert expected routes directly within the top three results
- proxy production-origin artifact requests to the local fixture - stabilize AI tail rendering and shell interaction selectors - validate ranking routes without assuming option markup - keep known upstream axe findings as an explicit baseline
- silence fixture server logs without hiding test failures - persist a non-active top-level sidebar disclosure - resolve expected search titles from the built index - click real palette rows while asserting top-three ranking
- persist sidebar state after OINK completes its disclosure update - wait for asynchronous Lunr results before ranking assertions - remove animation transients from color-contrast scans
- wait for scoped persistence initialization before disclosure clicks - exercise the same non-active Develop group in both locales - scan contrast only after theme and motion styles settle
- select the visible desktop collapse button explicitly - avoid the hidden restore control that shares the data hook - retain the collapse, inert, restore, and focus assertions
- install cwebp and dwebp before source validation - verify both tools are available in the prepare job - keep the Hugo and WebP setup order under a static contract
- cover navigation, search, sidebar, and Community states - pair desktop and mobile routes across light and dark themes - keep all eight screenshots advisory and artifact-backed
- skip Community-only assertions on standalone PR-A artifacts - retain the blocking 5-3-2 and parity contract after PR-B integration - keep shared platform coverage active in both release phases
- skip fixed ranking assertions before PR-B metadata is present - retain native search coverage in standalone PR-A - run all 24 Top-3 cases automatically after integration
- keep historical selectors on the configured production origin - scope generated text corpus URLs for staging validation - reject parent symlinks before resolving removable outputs - cover selector and sentinel regressions
- keep native search retry DOM stable and keyboard usable - reset timed-out Kapa scripts and ignore late callbacks - source Kapa primary colors from the site theme token - limit backlinks to five before native expansion - cover pending, failure, and six-link fixtures
- pass the production historical origin through aggregation - revalidate downloaded artifacts with identical URL scope - cover the aggregate CLI and workflow contract
- group production runs by the asf-site target - isolate staging and pull request concurrency - guard the target-level concurrency contract
- keep the first search-index request failure explicit - delegate retries to the local aggregate remapper - avoid accidental dependence on deployed asset hashes
- keep the leading dash inside the argparse value - cover the exact workflow command form - validate the real parser accepts run-scoped suffixes
- generate a deterministic five-version logical route map - scope version choices to equivalent pages or explicit fallbacks - validate map targets and publish an aggregate audit copy - cover aliases, route drift, and missing-target contracts
- route all three version selectors through the page map - preserve query and hash only for equivalent targets - expose a localized one-time fallback status - align Palette execution with native anchor navigation - cover desktop, mobile, and fallback browser behavior
- normalize Hugo permalinks independently of the publish base - preserve logical route lookup for archived EN and CN pages - cover the historical permalink contract with a focused regression
- remove hard-coded runtime version order and refs - bind route generation and validation to the manifest - preserve introduction/readme across historical releases - derive workflow scopes and add browser regressions
- derive missing shared-page destinations from the active site origin - preserve production history selectors for latest-only staging - cover the full-staging 1.0 missing-page regression
- add one manifest-aware server and build wrapper - keep strict production flags and safe argument forwarding - verify generated configuration with isolated command doubles - document the unified bilingual development workflow
- derive native links and Palette choices from one target partial - emit equivalent and fallback flags during Hugo rendering - cover latest and historical bilingual wrapper builds - verify live server Palette navigation preserves page context
- derive the default version from the manifest - reject configuration and strict-build override forms early - keep supported base URL and port semantics aligned - cover negative invocations before either tool can run
- preserve Hugo-authored Palette options during artifact scoping - compare interactive native links with the route oracle - cover postprocess ownership and native drift regressions - exempt print-only outputs without native navigation
- prefer direct targets before consulting equivalence groups - accept exactly one non-null alternate target - preserve fallback behavior for absent or ambiguous routes - cover bilingual latest and historical wrapper output
- derive locale-safe route equivalence groups from reviewed aliases - fail closed on ambiguous route candidates - validate localized LLMSFULL and scoped social images - cover EN/CN selector query and hash preservation
- require aliases to reach a canonical page in the same artifact - resolve validated alias chains to their terminal logical ID - reject malformed equivalence types without traceback - cover missing targets and chained aliases
- inspect every locale-specific Source row - reject cross-origin and cross-version corpus entries - detect malformed and duplicate source metadata
- require canonical locale-bound LLMS source rows - reject ambiguous source URL encodings and delimiters - validate social metadata as same-artifact image targets
- identify alias pages before social metadata validation - exempt redirects only when both social tags are absent - retain strict validation for content and partial metadata - cover redirect, content, and partial-tag regressions
- derive error-document paths from published version metadata - validate 1.3 and 1.0 error pages in security-only mode - fail closed on malformed version metadata - cover complete five-version paths and malicious SEO tags
- require ordinary aliases to remain on the configured origin - enforce current-version scope and existing artifact targets - reuse strict alias checks during route-map generation - reject external, protocol, encoded, and ambiguous targets
- allow only the two reviewed archived home redirects - bind English and Chinese homes to exact shared roots - keep ordinary aliases on strict artifact-local validation - cover production and staging origins plus hostile targets
- reject HTTP aliases whose parsed authority is empty - block whitespace and control characters in redirect targets - cover malformed triple-slash HTTP and HTTPS forms - retain exact archive-home and artifact-local contracts
- reject HTTP(S) URLs without a parsed authority - block whitespace, controls, and backslashes before URL resolution - apply browser-parser shape checks in security-only scans - cover triple-slash, opaque, and control-character forms
- apply one browser-safe shape policy to HTML and CSS tokens - cover srcset, object, media, inline CSS, and stylesheets - run the complete rendered security scan during artifact validation - reject malformed resource URLs in security-only publication gates
- distinguish contact links from executable resource URLs - reject non-HTTP schemes across HTML and CSS resources - preserve explicit mailto and tel anchor behavior - cover script, iframe, object, media, srcset, and CSS inputs
- classify navigation, active resources, and contact URLs by context - reject ping and base while validating form and SVG request targets - cover fetch-capable link relations including prefetch and prerender - allow only passive base64 raster data images - mirror active attributes in version scoping and artifact validation
- tokenize CSS requests and reject ambiguous browser URL surfaces - scope and validate srcset, SVG, and runtime action URLs - externalize Bootstrap data SVGs into versioned local assets - copy shared shell images into every historical build
- distinguish media source requests from picture candidates - mark rendered authored content independently of container nesting - reject premature boundary exits and spoofed markers
- keep media source classification across mismatched end tags - mark ordinary and print-rendered authored content boundaries - cover landing print content without weakening active-markup checks
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The biggest simplification is tests/e2e/workflow-contract.test.cjs — 88 lines of regular expressions matched against the literal text of .github/workflows/hugo.yml, of which only the contents: write check encodes a real invariant; the rest fail on reformatting and nothing else. Three smaller ones follow: ~60 lines of scripts/hugo.sh re-parse Hugo's own CLI to forbid flags Hugo's last-wins pflag already lets you override, hooks/body-end.html hardcodes Chinese UI copy two lines after calling T for the English, and version-target.html scans all 195 route entries up to twenty times per page. Evidence: reviewed the full diff at head 25f3e8c (git diff --stat d88167dd797efafea50cec59909d57849703fde5 pr472 — 87 files, +10024/-365) in a worktree at that SHA; parsing data/version_routes.json gives 195 logical entries and 28 path deviations, all from the api-preformance rename; grep -rn version-target layouts/ returns four call sites; grep -c "" i18n/en.yaml i18n/cn.yaml returns 1 and 236; .github/workflows/hugo.yml:353 runs npm run test:ci, which includes the contract test. The route map, the five-version manifest, the CSP/SVG allowlist in dist/validate-site-output.py, the local Bootstrap SVGs and the disabled-by-default Kapa adapter are all justified in the PR description and are not raised.
| fs.readFileSync(path.resolve(__dirname, "../../versions.json"), "utf8") | ||
| ); | ||
|
|
||
| test("each build fetches and verifies its immutable matrix SHA", () => { |
There was a problem hiding this comment.
- This one matches
/git fetch --no-tags origin "\$RESOLVED_SHA"/and/git cat-file -e .../— the shell block quoted back at itself. aggregate binds the option-looking artifact suffixasserts--artifact-suffix="..."anddoesNotMatchthe space-separated spelling. That is an equals-sign check.prepare pins Hugo and WebP tools before source validatorscomparesworkflow.indexOf("name: Setup Hugo Extended")againstindexOf("name: Validate source and version tooling"). Rename a step and it fails.concurrency serializes every writeris a[\s\S]*regex over the group expression; reorder two keys and it fails.
The one invariant worth protecting is in only publish receives write permission — but it is spelled against the inline-flow form permissions: { contents: read }, so converting that to block style turns a no-op reformat into a red build. CI runs this on every PR (npm run test:ci, .github/workflows/hugo.yml:353), so those false failures are live, and the next person to touch the workflow pays for them.
Requested change: keep the write-permission check and delete the other five cases:
test("only publish receives write permission", () => {
assert.equal((workflow.match(/contents: write/g) || []).length, 1);
assert.match(workflow, /publish:[\s\S]*?contents: write/);
});About ten lines instead of eighty-eight, and it still catches the thing that would actually matter. git diff already proves the rest.
| repo_dir=$(dirname "$script_dir") | ||
| cd "$repo_dir" | ||
|
|
||
| reject_argument() { |
There was a problem hiding this comment.
scripts/versioning.py config and pass it as --config hugo.yaml,$tmp — is about thirty lines. The other sixty are a hand-rolled parser for Hugo's own flag syntax: an expect_value state machine, four spellings each of --baseURL/-b and --port/-p, and an eleven-entry reject list (--config, --gc, --minify, --panicOnWarning, --logLevel, --environment, …).
The reject list guards nothing. Lines 145-162 already put the wrapper's flags before "$@", and Hugo's pflag is last-wins, so a contributor passing --minify=false already works — this turns that into exit 2. It is a local preview script, not a trust boundary; nobody loses data because someone previewed with --logLevel debug.
It is also most of what scripts/test_hugo_wrapper.py (219 lines) tests: test_owned_hugo_arguments_are_rejected_before_any_tool_runs, test_site_origin_environment_cannot_conflict_with_base_url, and test_base_url_and_port_keep_generated_and_hugo_origins_aligned exist only for this machinery.
Requested change: delete reject_argument() and the reject case arms (lines 29-33 and 51-64) with those three tests, and collapse the sniff to the two spellings README documents:
for argument in "$@"; do
case "$argument" in
--baseURL=*) base_url=${argument#*=} ;;
-p) port=next ;;
*) [ "$port" = next ] && port=$argument ;;
esac
doneHG_DOC_SITE_ORIGIN already covers every other origin case and is already documented in contribution.md.
| {{- $sourceGroup := index $ai.sourceGroups $lang -}} | ||
| {{- $themeColor := index .Site.Params.ui "theme_color" -}} | ||
| {{- $historical := ne (.Site.Params.version | default "latest") "latest" -}} | ||
| {{- $labels := cond (eq $lang "cn") |
There was a problem hiding this comment.
T "ui_version_fallback" — and then line 6, two lines earlier, hardcodes its Chinese counterpart inline rather than adding the key to i18n/cn.yaml.
Both translation files are right there: this PR creates i18n/en.yaml, and i18n/cn.yaml already carries 236 entries. Every other partial in this PR uses T — backlinks.html calls it five times. The practical cost is that the Chinese copy for a user-facing feature is not in the file a translator opens, so 询问 AI and AI 暂时不可用,本地搜索不受影响。 will quietly drift.
Requested change: add ui_ask_ai, ui_ask_ai_description, ui_ask_ai_latest, ui_retry, ui_ai_error and ui_version_fallback to both YAML files, then:
{{- $labels := dict "ask" (T "ui_ask_ai") "description" (T "ui_ask_ai_description") "latest" (T "ui_ask_ai_latest") "retry" (T "ui_retry") "error" (T "ui_ai_error") -}}
That deletes both cond branches here and lines 4-9 above, since $fallbackMessage becomes T "ui_version_fallback" unconditionally.
| {{- $docsSuffix := cond (eq $locale "cn") "/cn/docs" "/docs" -}} | ||
| {{- $root := strings.TrimSuffix $docsSuffix (strings.TrimSuffix "/" $versionURL) -}} | ||
| {{- if and $rawURL (strings.HasPrefix $relative $docsPrefix) -}} | ||
| {{- $pages := hugo.Data.version_routes.pages | default dict -}} |
There was a problem hiding this comment.
range on line 19 reverse-maps this page's path to a logical route ID by walking every entry in hugo.Data.version_routes.pages — 195 entries at head — and there is no break, so it runs to completion after it has already found the match.
That would be fine once per page. It is not once per page: version-target.html is called once per version from navbar.html:87, navbar.html:174, shell/sidebar-panel.html:69, and actions/manifest.html:3 via version-options.html. Five versions × four call sites ≈ 3,900 map comparisons per page, and the PR's own validation reports 1,082 HTML pages per aggregate — so roughly four million, twenty times more than the one scan the page needs.
Deriving the ID from the path instead is not an option and I am not asking for it: 28 of the route cells deviate from lang:path (all api-preformance → api-performance), which is the reason the map exists. But the answer is the same for all twenty calls on a page.
Requested change, no new file and no route-map schema change:
{{- $logicalID := $p.Store.Get "hgVersionLogicalID" -}}
{{- if not $logicalID -}}
{{- $logicalID = "" -}}
{{- range $candidateID, $candidateRoutes := $pages -}}
{{- if eq (index $candidateRoutes $currentVersion) $relative -}}{{- $logicalID = $candidateID -}}{{- end -}}
{{- end -}}
{{- $p.Store.Set "hgVersionLogicalID" $logicalID -}}
{{- end -}}
.Store is already the pattern here — actions/manifest.html:1 reads .Store.Get "tdOutputFormat". Adding {{ break }} inside the range is a second one-word win. A byPath index from generate_version_routes would be tidier, but validate_version_routes asserts an exact top-level key set, so that one costs a validator and test change too.
Before → after
versions.jsondrives one immutablelatest / 1.7 / 1.5 / 1.3 / 1.0release contract, including selectors, aliases, SEO, and aggregate metadata.Main changes
sidebar state, mobile isolation, focus restoration, theme, image zoom,
Backlinks, and Blog copy-link behavior.
llms-full.txtfiles without adding them tohistorical releases or making Kapa a build dependency.
latest-only staging. A deterministic 195-page route map keeps users on the
equivalent logical page when it exists and gives a locale-correct,
one-time-explained Docs-root fallback when it does not.
short-lived artifacts, aggregate/security validation, Node 24, Playwright,
Chromium, and fixed ASF publication targets.
image attributes, runtime URLs, and authored content boundaries. Bootstrap
control SVGs and shared historical assets remain local and version-complete.
Ask AI direction
The selected design combines a contextual Search Tail with a restrained
Floating Launcher. It excludes a persistent side panel and the old red/pink
assistant treatment.
Full decision record: #467 (comment)
Validation
bash dist/validate-links.shpython3 -m unittest discover -s scripts -p 'test_*.py' -v— 124 passednode --test tests/ui-ai/*.test.cjs— 23 passednode --test tests/e2e/workflow-contract.test.cjs— 6 passed10 error documents; all 195 logical route entries pass exact
forward/missing/reverse validation, including two explicit EN/CN renamed-page
equivalence groups
2 error documents; every historical selector remains on the production
origin
10 error documents; current and historical routes remain on staging
historical LLMSFULL outputs remain absent
version-scoped fallback images present; 59 static redirects are explicitly
distinguished from content pages
retain balanced markers; media nesting and duplicate attributes fail closed
Delivery boundaries
CSP hosts, and live privacy/failure smoke checks pass.