From a32b915417a8d79989c5f7b69582cdc580d95b8b Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 2 Mar 2026 18:14:11 +0100 Subject: [PATCH 1/5] Strip Scroll to Text Fragment anchors in link checks Scroll to Text Fragment anchors (#:~:text=...) are a browser-only feature that lychee cannot verify in static HTML. Without this fix, the "other fragments" remap rule catches these URLs first and redirects them to raw.githubusercontent.com, where the fragment validation fails. This was observed in open-telemetry/opentelemetry-java-instrumentation where a link to build.gradle#:~:text=extendedAgent fails in CI but works locally (because build_remap_args skips when on the base branch). Fixes the issue by adding explicit rules (in both repo-specific and global remaps) to strip the fragment before the catch-all fragment rule matches. Signed-off-by: Gregor Zeitlinger --- tasks/lint/links.sh | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tasks/lint/links.sh b/tasks/lint/links.sh index a6102de..3c34ab7 100755 --- a/tasks/lint/links.sh +++ b/tasks/lint/links.sh @@ -25,9 +25,10 @@ eval "lychee_args=(${usage_lychee_args:-})" # https://github.com/lycheeverse/lychee/issues/1729). # # Lychee uses first-match-wins for remaps, so order matters: -# 1. Line-number anchors → strip fragment, remap to head branch -# 2. Other fragments → remap to raw.githubusercontent.com -# 3. No fragment → remap to head branch (existing behavior) +# 1. Line-number anchors → strip fragment, remap to head branch +# 2. Scroll to Text Fragments → strip fragment, remap to head branch +# 3. Other fragments → remap to raw.githubusercontent.com +# 4. No fragment → remap to head branch (existing behavior) # # Set LYCHEE_SKIP_GITHUB_REMAPS=true to skip the GitHub-specific remaps # emitted by this function (escape hatch if they cause unexpected behavior; @@ -72,18 +73,23 @@ build_remap_args() { local base_url="https://github.com/${repo}" local head_url="https://github.com/${head_repo}" - # /blob/ URLs — three rules, order matters (first-match-wins): + # /blob/ URLs — four rules, order matters (first-match-wins): # 1. Line-number anchors (#L123): strip fragment, remap to head branch echo "--remap" echo "^${base_url}/blob/${base_ref}/(.*?)#L[0-9]+\$ ${head_url}/blob/${head_ref}/\$1" - # 2. Other fragment URLs (#section): remap to raw.githubusercontent.com + # 2. Scroll to Text Fragment anchors (#:~:text=...): browser-only, + # strip fragment, remap to head branch + echo "--remap" + echo "^${base_url}/blob/${base_ref}/(.*?)#:~:text=.*\$ ${head_url}/blob/${head_ref}/\$1" + + # 3. Other fragment URLs (#section): remap to raw.githubusercontent.com # so lychee can verify the fragment in raw content echo "--remap" echo "^${base_url}/blob/${base_ref}/(.*#.*)\$ https://raw.githubusercontent.com/${head_repo}/${head_ref}/\$1" - # 3. Non-fragment URLs: branch-remap only (existing behavior) + # 4. Non-fragment URLs: branch-remap only (existing behavior) echo "--remap" echo "^${base_url}/blob/${base_ref}/(.*)\$ ${head_url}/blob/${head_ref}/\$1" @@ -105,6 +111,9 @@ build_remap_args() { # - Line-number anchors (#L123, #L10-L20): rendered by JavaScript, # lychee cannot verify them. We strip the fragment so the file # itself is still checked. +# - Scroll to Text Fragment anchors (#:~:text=...): browser-only, +# lychee cannot verify them. We strip the fragment so the file +# itself is still checked. # - Issue comment anchors (#issuecomment-*): rendered by JavaScript, # lychee cannot verify them. The fragment is stripped so the # issue/PR page itself is still checked. @@ -122,6 +131,11 @@ build_global_github_args() { # shellcheck disable=SC2016 # single quotes are intentional: these are regex capture groups, not shell vars echo '^https://github.com/([^/]+/[^/]+)/blob/([^/]+)/(.*?)#L[0-9]+.*$ https://github.com/$1/blob/$2/$3' + # Strip Scroll to Text Fragment anchors from /blob/ URLs (browser-only, not in static HTML) + echo "--remap" + # shellcheck disable=SC2016 # single quotes are intentional: these are regex capture groups, not shell vars + echo '^https://github.com/([^/]+/[^/]+)/blob/([^/]+)/(.*?)#:~:text=.*$ https://github.com/$1/blob/$2/$3' + # Strip issue comment anchors (JS-rendered, not in static HTML). # The issue page is still checked, just not the fragment. # We use --remap instead of --exclude because CLI --exclude From 560ee4e561acbca07fbbe5f5f2d5fbb43a1c46b9 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 2 Mar 2026 18:34:50 +0100 Subject: [PATCH 2/5] Add test cases for Scroll to Text Fragment anchors Signed-off-by: Gregor Zeitlinger --- tests/test-links.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test-links.md b/tests/test-links.md index 788fd93..7fb8ea7 100644 --- a/tests/test-links.md +++ b/tests/test-links.md @@ -9,6 +9,14 @@ these links verify that each remap rule works correctly during CI. - [README.md#L1](https://github.com/grafana/flint/blob/main/README.md#L1) - [links.sh#L6](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#L6) +## Scroll to Text Fragment anchors (`#:~:text=...`) — fragment stripped, file checked on PR branch + +- [links.sh text fragment](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#:~:text=build_remap_args) + +## External Scroll to Text Fragment anchors — fragment stripped globally + +- [okhttp text fragment](https://github.com/square/okhttp/blob/master/README.md#:~:text=OkHttp) + ## Section fragments (`#section`) — remapped to raw.githubusercontent.com - [CHANGELOG.md heading](https://github.com/grafana/flint/blob/main/CHANGELOG.md#changelog) From ca93a20bf54ced5fd06310674e3b206883a6ea6d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 2 Mar 2026 18:45:46 +0100 Subject: [PATCH 3/5] Fix repo-specific line-number range anchors (#L10-L20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo-specific remap used #L[0-9]+$ which didn't match range fragments like #L10-L20. These would fall through to the "other fragments → raw.githubusercontent.com" rule where they also can't be verified. Changed to #L[0-9]+.*$ to match ranges, consistent with the global remap rule. Signed-off-by: Gregor Zeitlinger --- tasks/lint/links.sh | 8 ++++---- tests/test-links.md | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tasks/lint/links.sh b/tasks/lint/links.sh index 3c34ab7..c562cc2 100755 --- a/tasks/lint/links.sh +++ b/tasks/lint/links.sh @@ -75,9 +75,9 @@ build_remap_args() { # /blob/ URLs — four rules, order matters (first-match-wins): - # 1. Line-number anchors (#L123): strip fragment, remap to head branch + # 1. Line-number anchors (#L123, #L10-L20): strip fragment, remap to head branch echo "--remap" - echo "^${base_url}/blob/${base_ref}/(.*?)#L[0-9]+\$ ${head_url}/blob/${head_ref}/\$1" + echo "^${base_url}/blob/${base_ref}/(.*?)#L[0-9]+.*\$ ${head_url}/blob/${head_ref}/\$1" # 2. Scroll to Text Fragment anchors (#:~:text=...): browser-only, # strip fragment, remap to head branch @@ -95,9 +95,9 @@ build_remap_args() { # /tree/ URLs — two rules: - # 1. Line-number anchors: strip fragment, remap to head branch + # 1. Line-number anchors (#L123, #L10-L20): strip fragment, remap to head branch echo "--remap" - echo "^${base_url}/tree/${base_ref}/(.*?)#L[0-9]+\$ ${head_url}/tree/${head_ref}/\$1" + echo "^${base_url}/tree/${base_ref}/(.*?)#L[0-9]+.*\$ ${head_url}/tree/${head_ref}/\$1" # 2. Non-fragment URLs: branch-remap only echo "--remap" diff --git a/tests/test-links.md b/tests/test-links.md index 7fb8ea7..7fcb853 100644 --- a/tests/test-links.md +++ b/tests/test-links.md @@ -4,10 +4,11 @@ These links exercise the GitHub URL remap rules in `tasks/lint/links.sh`. On PR branches, lychee rewrites `blob/main/` URLs to the PR branch — these links verify that each remap rule works correctly during CI. -## Line-number anchors (`#L123`) — fragment stripped, file checked on PR branch +## Line-number anchors (`#L123`, `#L10-L20`) — fragment stripped, file checked on PR branch - [README.md#L1](https://github.com/grafana/flint/blob/main/README.md#L1) - [links.sh#L6](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#L6) +- [links.sh#L6-L10](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#L6-L10) ## Scroll to Text Fragment anchors (`#:~:text=...`) — fragment stripped, file checked on PR branch From b9b8d52b55737202ecbf1f154162a4b01c5129f9 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 2 Mar 2026 18:55:30 +0100 Subject: [PATCH 4/5] Fix editorconfig line-length violation in test-links.md Signed-off-by: Gregor Zeitlinger --- tests/test-links.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test-links.md b/tests/test-links.md index 7fcb853..6d837fa 100644 --- a/tests/test-links.md +++ b/tests/test-links.md @@ -12,7 +12,9 @@ these links verify that each remap rule works correctly during CI. ## Scroll to Text Fragment anchors (`#:~:text=...`) — fragment stripped, file checked on PR branch + - [links.sh text fragment](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#:~:text=build_remap_args) + ## External Scroll to Text Fragment anchors — fragment stripped globally From 1156abc755f2c3d8ca58d4bea7fc5f72053ca920 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 2 Mar 2026 19:09:35 +0100 Subject: [PATCH 5/5] docs: add Scroll to Text Fragment and line range coverage to README Signed-off-by: Gregor Zeitlinger --- README.md | 29 ++++++++++++++++++++--------- tests/test-links.md | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1d86131..0588f32 100644 --- a/README.md +++ b/README.md @@ -212,20 +212,23 @@ to the base branch resolve against the PR branch instead. This ensures that links like `/blob/main/README.md` don't break when the file was added or moved in the PR. -For `/blob/` URLs, three ordered remap rules are applied +For `/blob/` URLs, four ordered remap rules are applied (lychee uses first-match-wins): -1. **Line-number anchors** (`#L123`): GitHub renders these with - JavaScript, so lychee can never verify the fragment. The anchor +1. **Line-number anchors** (`#L123`, `#L10-L20`): GitHub renders + these with JavaScript, so lychee can never verify the fragment. + The anchor is stripped and the file is checked on the PR branch. +2. **[Scroll to Text Fragment][stf] anchors** (`#:~:text=...`): + Browser-only feature, not present in static HTML. The anchor is stripped and the file is checked on the PR branch. -2. **Other fragment URLs** (`#section`): Remapped to +3. **Other fragment URLs** (`#section`): Remapped to `raw.githubusercontent.com` where lychee can verify the fragment in the raw file content (workaround for [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)). -3. **Non-fragment URLs**: Remapped from the base branch to the PR +4. **Non-fragment URLs**: Remapped from the base branch to the PR branch (the original behavior). -For `/tree/` URLs, rules 1 and 3 apply (no raw remap needed). +For `/tree/` URLs, rules 1 and 4 apply (no raw remap needed). **Global GitHub URL handling:** @@ -237,6 +240,9 @@ two patterns that affect ALL GitHub URLs (any repository): JS-rendered line-number fragment is skipped. This means consuming repos don't need to exclude these in their `lychee.toml`. +- **Scroll to Text Fragment anchors** (`#:~:text=...`): Stripped + from any GitHub `/blob/` URL. These are a browser-only feature + not present in static HTML. - **Issue comment anchors** (`#issuecomment-*`): The fragment is stripped so the issue/PR page is still checked, but the JS-rendered comment anchor is skipped. @@ -254,11 +260,14 @@ via `--remap` arguments: [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)** — flint remaps fragment URLs to `raw.githubusercontent.com` for the current PR's head branch, and strips line-number - anchors globally. + and Scroll to Text Fragment anchors globally. - **`#issuecomment-*` excludes** — flint strips the fragment via remap so the issue/PR page is still checked. -- **`#L\d+` line-number excludes** — flint strips the fragment - via remap so the file is still checked. +- **`#L\d+` / `#L\d+-L\d+` line-number excludes** — flint strips + the fragment via remap so the file is still checked. +- **`#:~:text=...` [Scroll to Text Fragment][stf] excludes** — + flint strips the fragment via remap so the file is still + checked. Note: flint uses `--remap` (not `--exclude`) for these because lychee's CLI `--exclude` flags override config file excludes @@ -444,3 +453,5 @@ When conventional commits land on `main`, Release Please opens > **Note:** CI checks don't trigger automatically on release-please > PRs because they are created with `GITHUB_TOKEN`. To run CI, > either click **Update branch** or **close and reopen** the PR. + +[stf]: https://developer.mozilla.org/en-US/docs/Web/URI/Fragment/Text_fragments diff --git a/tests/test-links.md b/tests/test-links.md index 6d837fa..fe08163 100644 --- a/tests/test-links.md +++ b/tests/test-links.md @@ -13,6 +13,7 @@ these links verify that each remap rule works correctly during CI. ## Scroll to Text Fragment anchors (`#:~:text=...`) — fragment stripped, file checked on PR branch + - [links.sh text fragment](https://github.com/grafana/flint/blob/main/tasks/lint/links.sh#:~:text=build_remap_args)