Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found - #21694

Merged
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives
Aug 10, 2026
Merged

Fix the nightly link check: tell a broken runner from a dead link, repair every dead link it found#21694
shoumikhin merged 1 commit into
mainfrom
shoumikhin/linkcheck-fewer-false-positives

Conversation

@shoumikhin

@shoumikhinshoumikhin commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The nightly link-check / lint-urls job has been red for weeks. There are two separate problems behind that, and this PR fixes both.

Problem 1: the script cannot tell a dead host from a broken runner

scripts/lint_urls.sh gets 000 back from curl when there is no HTTP response at
all. That happens for two very different reasons: the host is dead, or the runner
has no working egress. The script treated both as WARN and moved on.

That is unsafe in both directions:

  • A genuinely dead host is silently tolerated. The 000 case is exactly the one we
    most want to catch.
  • More importantly it also disables the pull request gate. .github/workflows/lint.yml
    runs this same script in diff mode on every pull request, where the only URLs
    checked are the ones that pull request adds. A new link with a typo in the host
    name gives 000 and passes today. A runner with no egress makes every link WARN
    and the whole job green. Both were reproduced locally.

The fix

On 000, probe one known-good URL, https://api.github.com, which the runner
already depends on:

  • probe also returns 000, so this runner has no egress: WARN, as before.
  • probe returns anything else, so egress works and the problem is the host: retry
    the URL once with a 60 second timeout, then judge it on that result, which for a
    dead host means FAIL.

One deliberate addition worth calling out: the retry with the longer timeout is
more than the minimum needed to close the hole. It is there because the same URLs
were sampled across six nights of nightly logs and a small number of them, one or
two per night out of about 2100, return 000 once and then answer normally. The
default timeout is 10 seconds. Without the retry, this change would turn those
into hard failures and the nightly would stay red for no good reason.

Also removed: the check-host.net fallback

When the first request failed, the script asked check-host.net whether the URL
was reachable from elsewhere and could then overwrite the verdict. That is
removed. Note this makes the script stricter, not more lenient: on a 404 or a 500
the fallback could turn a FAIL into an OK. Dropping it means a real 404 stays a
404, and it also removes a third-party service from the critical path of a lint
job.

Problem 2: eight actually dead links

With problem 1 fixed the nightly would still be red, because there really are dead
links in the tree. The most recent nightly on main
(31350839957)
reported 7 x FAIL 404 and 1 x FAIL 000. All eight are handled here:

filelinkwhat happenedfix
setup.pypybind/cmake_example blob/master/setup.pythat project deleted its setup.py, so a master link cannot workpin to the last revision that still had it
.ci/docker/common/install_openssl.shpytorch/pytorch blob/main/.ci/docker/common/install_openssl.shthe file moved on mainpin to a revision where it exists
.ci/scripts/test_llava.shWikimedia basketball photothe photo was renamed on Wikimedia, not deletedpoint at the same photo under its current name
docs/source/kernel-library-custom-aten-kernel.mdpytorch.org/cppdocs/library.html#...page and anchor both gonecurrent cppdocs library index
test/models/export_program.pypytorch.org/cppdocs/notes/tensor_indexing.htmlgone, and it was only mentioned in a commentdrop the URL, keep the sentence
docs/source/success-stories.mdan App Store listingapp delisteddrop the link, keep the text
README.mdgithub.com/pytorch/executorch/stargazersreturns 404 to an unauthenticated bot, works fine in a browser@lint-ignore
backends/arm/scripts/toolchain_utils.shmusl.cc toolchain tarballserves fine to developers, has never once answered from a GitHub hosted runner@lint-ignore

Two more URLs are cleaned up along the way: a truncated Qualcomm SDK URL in
backends/qualcomm/scripts/download_qnn_sdk.py that was never a real link, and a
Qwen issue link in examples/models/llama/runner/generation.py pointing at the
old repository name.

On the Wikimedia one specifically: examples/models/llava/README.md already
embeds the renamed photo and documents the caption the model produces for it,
which matches the EXPECTED_PREFIX the script asserts. So this restores the
original input rather than substituting a different picture. Worth knowing, and
the reason this is low risk: no workflow runs .ci/scripts/test_llava.sh. Both
pull.yml and trunk.yml carry the comment "llava gives segfault so not
covering", so llava is not in CI at all right now.

How this was verified

A whole-tree scan is nightly-only, so a pull request run here would only check the
handful of URLs this PR adds. To get real numbers, _link_check.yml was dispatched
in whole-tree mode against this branch's contents
(31417702080):

main, nightlythis branch, whole tree
URLs scanned21482147
OK21162116
WARN2431
FAIL80

Reconciling the two totals, since they should not match exactly:

  • 11 URLs leave the scan: 5 replaced by a working equivalent, 4 deleted outright,
    and 2 now marked @lint-ignore.
  • 6 join it: the 5 replacements, plus https://api.github.com, which is now
    written in lint_urls.sh as the probe target and so gets scanned itself.
  • That is 2148 minus 5, which is 2143. The remaining 4 are URLs that landed on main
    during the day, in examples/models/muse-glimmer/, and are unrelated to this
    change. All 4 pass.

The 7 extra WARNs are the same thing that produces the existing 24: sites that
return 403 to an automated client. Between the two runs, cppreference.com,
stackoverflow.com and vulkan.org happened to answer 403 instead of 200.
Those are already treated as warnings and are not affected by this PR.

The 000 handling itself was also exercised directly, in a scratch repository:

  • no egress at all: WARN 000, exit 0, matching a broken runner.
  • egress working, host that does not exist: FAIL 000, exit 1.
  • OK 200, WARN 403 and FAIL 404 all still behave as before.
  • diff mode still only looks at added lines.

CopilotAI lite review requested due to automatic review settings August 8, 2026 23:03
@pytorch-bot

pytorch-botBot commented Aug 8, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21694

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 114 Pending

As of commit f4bd5e6 with merge base 9cd0c12 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 8, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from dbb9910 to 091bebfCompareAugust 8, 2026 23:33
CopilotAI review requested due to automatic review settings August 8, 2026 23:33
CopilotAI reviewed Aug 8, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 091bebf to 8ca5333CompareAugust 9, 2026 04:39
CopilotAI review requested due to automatic review settings August 9, 2026 04:39
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhinshoumikhin changed the title Make the nightly link check trustworthy: stop failing on no-response, fix the one real dead linkFix the nightly link check: stop failing on no-response, repair every dead link it foundAug 9, 2026
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 8ca5333 to 6813118CompareAugust 9, 2026 04:51
CopilotAI review requested due to automatic review settings August 9, 2026 04:51
@shoumikhin
shoumikhin requested a review from psiddh as a code ownerAugust 9, 2026 04:51
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 9, 2026 05:55
CopilotAI reviewed Aug 9, 2026

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 10, 2026 05:51
@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 6c4e82c to 70e4370CompareAugust 10, 2026 05:51

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhinforce-pushed the shoumikhin/linkcheck-fewer-false-positives branch from 70e4370 to f4bd5e6CompareAugust 10, 2026 18:07
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added ciflow/trunk module: arm Issues related to arm backend labels Aug 10, 2026
@shoumikhinshoumikhin changed the title Fix the nightly link check: stop failing on no-response, repair every dead link it foundFix the nightly link check: tell a broken runner from a dead link, repair every dead link it foundAug 10, 2026
@shoumikhin
shoumikhin merged commit 52467d5 into mainAug 10, 2026
689 of 690 checks passed
@shoumikhin
shoumikhin deleted the shoumikhin/linkcheck-fewer-false-positives branch August 10, 2026 19:04
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on eight links pytorch#21694 had already fixed or ignored.
The workflow now resolves the merge base and passes it down. That is the half
that matters for branches already open: on a pull request the reusable
workflow resolves from the merge commit, so it carries this fix even though
scripts/ still comes from the branch itself. The scripts switch to three dot
ranges so running them by hand behaves the same way, and the checkouts fetch
real history, without which no merge base exists to compute.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit to psiddh/executorch that referenced this pull request Aug 12, 2026
The URL, xref, and file size linters diffed base..head, where base is the tip
of the base branch rather than the point the branch left it. A branch cut
before recent commits still carries the lines those commits replaced, so
against the newer tip its old copies read as additions and the branch is
blamed for links someone else already repaired. pytorch#21729 failed exactly that
way, on nine links pytorch#21694 had already fixed or ignored.
The workflow resolves the merge base and passes it down. That is the half that
matters for branches already open: on a pull request the reusable workflow
resolves from the merge commit, so it carries this fix even though scripts/
still comes from the branch itself. The scripts switch to three dot ranges so
running them by hand behaves the same way.
Computing a merge base needs history, so the pull request path checks out in
full, restoring what pytorch#17682 traded away for speed at a measured 25s of wall
clock. Pushes and the nightly whole tree scan cannot use a merge base and stay
shallow. A missing merge base leaves the range unset for a whole tree scan
rather than substituting the base tip, which two of the three scripts accept
and then quietly pass.
Authored with Claude Code (Claude Opus 5).
psiddh added a commit that referenced this pull request Aug 12, 2026
…21765)
### Summary
The URL, xref, and file-size linters diffed `base..head`, where `base`
is the **tip** of the base branch rather than the point the branch left
it. A branch cut before recent commits still carries the lines those
commits replaced, so against the newer tip its old copies read as
additions, and the branch gets blamed for links someone else already
repaired.
#21729 failed exactly this way. It touches 18 files and adds no URLs at
all, but the two-dot diff scoped the lint to 143 files and flagged nine
links that #21694 had already fixed or `@lint-ignore`d. #21707 is
starker: one Python file with no URLs in it, 199 files linted, the same
nine failures.
```
base..head 143 files <- what CI linted
base...head 18 files <- what the PR actually changes
```
The stray `jq: parse error` lines in #21729's log are the same symptom
from the other direction: the job runs the branch's own pre-#21694 copy
of `lint_urls.sh`.
### Fix
The workflow resolves the merge base and passes it down. That is the
half that matters for branches already open: on a `pull_request` the
reusable workflow resolves from the merge commit, so it carries this fix
even though `scripts/` still comes from the branch itself. The scripts
also switch to three-dot ranges so `./scripts/lint_urls.sh main HEAD` by
hand behaves the same. `lint_xrefs.sh` and `lint_file_size.sh` had the
identical bug and get the identical change.
**Cost, stated plainly.** A merge base needs real history, so this
restores `fetch-depth: 0` on the `pull_request` path — line for line
what #17682 removed in February for speed. Measured on this branch, that
is ~25s of added wall clock and ~79s of runner time across the three
concurrent jobs. #17682's 6min → 10s was mostly the runner and Docker
change rather than the fetch depth, though the commit changes both at
once and I can't fully separate them. Pushes and the nightly whole-tree
scan cannot use a merge base and stay shallow:
```yaml
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
```
The quotes matter — bare `0` is falsy in GitHub expressions, so `&& 0 ||
1` always yields `1` and would silently disable the fix.
**No silent fallback.** When there is no merge base there is no usable
range, so the range is left unset and the linters scan the whole tree.
Substituting the base tip instead produces a range the scripts fail on
quietly: verified against unrelated histories, `lint_urls.sh` and
`lint_xrefs.sh` both exit **0** having checked nothing, while
`lint_file_size.sh` exits 128 and the wrapper reports "some files exceed
the 1 MB limit", which is not what happened. Unset args are loud instead
— verified rc=1 on a tree containing a dead link.
**`--no-color`.** Both `git diff` calls now pass it, matching the `git
grep --no-color` two lines below. With `color.ui = always` in a
developer's config, added lines arrive wrapped in escape sequences,
`grep -E '^\+'` matches nothing, and the check passes having found
nothing (measured: 2 matches → 0).
### Test plan
`.ci/scripts/tests/test_link_check_diff_selection.py` builds a diverged
history where `main` repairs bad links and shrinks an oversized file
while the feature branch simply predates all of it. Four fixture files
each pin a different part, and `curl` is stubbed so there is no network:
| Fixture | Pins |
|---|---|
| `both_sides.md` — edited on both branches | the per-file diff range |
| `big.bin` — oversized at the branch point, shrunk on main |
`lint_file_size.sh`'s range |
| `colorful.gitconfig` — `color.ui = always` | `--no-color` |
| `base_only.md` / `feature_only.md` | that main-only changes stay
invisible and the branch's own additions are still checked |
```
pytest .ci/scripts/tests/test_link_check_diff_selection.py # 4 passed
```
Mutating each changed line individually:
```
inner per-file range -> .. 3 failed caught
lint_file_size range -> .. 1 failed caught
drop --no-color 1 failed caught
file-selection range -> .. 4 passed equivalent mutant, see below
```
The file-selection range is not pinned because it cannot be: with the
per-file diff at three dots, the extra files it selects produce empty
diffs. Replayed against #21707's real 199-file range, both variants emit
byte-identical output. It is changed for consistency, not behaviour.
**Narrowing the scope must not blunt the check**, so each linter also
has a positive control where the branch itself adds the bad thing:
```
lint_urls adds a dead URL -> rc=1, reports example.invalid/dead
lint_xrefs adds a broken reference -> rc=1, reports sub/missing.md
lint_file_size adds a 1MB+ file -> rc=1, reports feature_big.bin
```
End to end on real content: a synthetic commit on top of `main` that
puts #21694's nine repaired links back, as if a PR had added them, gives
`rc=1` with 9 FAIL and 5 OK against the live network. Incidentally
`musl.cc` answered this time where CI saw `000`, and
`pybind/cmake_example` now 404s where CI saw `301` — which is the
retry-then-WARN path earning its keep.
Also replayed #21729's and #21707's exact CI refs through the fixed
scripts (exit 0 each), and ran all three linters plus `lintrunner`
against this PR's own diff.
### Known limitations
- The checkout is `head.sha`, so references still resolve against the
branch tree rather than the merge result — the mirror image of the bug
fixed here. Pre-existing and not worsened by this PR.
- The whole-tree branch swallows a `git grep` failure and exits 0, so on
a git built without PCRE the scan silently checks nothing. Reachable
locally, not on the runners, which is why the nightly scan works.
Pre-existing; worth a follow-up rather than widening this PR.
### Rollout
This does **not** repair a currently red run. A rerun keeps the original
`GITHUB_SHA`, and advancing the base alone does not fire `synchronize`,
so an already-open PR picks the fix up only on its next newly triggered
pull-request run — any push, or close/reopen. That is still cheaper than
a content rebase: no history rewrite, no conflicts, nothing to
re-review.
Supersedes #21762, which fixes the same bug but leaves the checkout
shallow and misses `lint_urls.sh`. Its reviewer's shallow-checkout point
is exactly right: a `--depth=1` fetch writes a shallow graft even into
an otherwise complete clone, after which `git merge-base` fails and
`A...B` is fatal, so the two halves of this change are a pair. The
regression test here is adapted from that PR.
Authored with Claude Code (Claude Opus 5).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.module: armIssues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@shoumikhin@JakeStevens