Explorar el Código

Add a link checker workflow using lychee (#9209)

* Add a link checker workflow using lychee

Depends on #9207 (merged)

https://github.com/lycheeverse/lychee-action

* Adjust cron to run once every month instead

* Apply suggestions

* Pass zizmor's pedantic audit

* Set `failIfEmpty: false` for lychee action

* Add offline Lychee check to catch broken links after renames

* Add workaround for `--files-from` option not respecting `extensions`

* Escape `?` correctly in `.lycheeignore`

* Don't accept timeouts, retry instead

* Bump link checker workflow to `ubuntu-26.04`

* Always scope to host correctly in `.lycheeignore`

* Set `hidden = true` in config to ensure that dotfiles are scanned

* Exclude issue template from the scan

* with open

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Inverle hace 19 horas
padre
commit
456b9fbff5
Se han modificado 3 ficheros con 211 adiciones y 0 borrados
  1. 113 0
      .github/workflows/link-checker.yml
  2. 54 0
      .lycheeignore
  3. 44 0
      lychee.toml

+ 113 - 0
.github/workflows/link-checker.yml

@@ -0,0 +1,113 @@
+name: Link Checker
+
+on:
+  push:
+    branches: [ edge ]
+  pull_request:
+    branches: [ edge ]
+  schedule:
+    - cron: '30 12 1 * *'
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+concurrency:
+  group: link-checker-${{ github.ref }}
+  cancel-in-progress: false
+
+jobs:
+  linkChecker:
+    name: linkChecker
+    runs-on: ubuntu-26.04
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
+          fetch-depth: 0
+
+      # Check local links across the repository first to catch broken links in
+      # files that are not part of the PR's changed files.
+      - name: Check local links (offline run)
+        if: github.event_name == 'pull_request'
+        uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
+        with:
+          fail: true
+          failIfEmpty: false
+          args: >
+            --offline
+            .
+
+      - name: Get changed files
+        if: github.event_name == 'pull_request'
+        env:
+          BASE_SHA: ${{ github.event.pull_request.base.sha }}
+          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+        run: |
+          git diff --name-only --diff-filter=ACMR \
+            "$BASE_SHA" \
+            "$HEAD_SHA" \
+            > changed-files.txt
+
+          # Lychee does not apply `extensions` from the config to files passed via `--files-from`.
+          # Filter the changed files so only allowed file types are checked.
+          python3 <<'PY'
+          import pathlib
+          import tomllib
+
+          with open("lychee.toml", "rb") as fp:
+              config = tomllib.load(fp)
+          extensions = {e.lstrip(".").lower() for e in config["extensions"]}
+
+          files = pathlib.Path("changed-files.txt").read_text().splitlines()
+          files = [
+              f for f in files
+              if (
+                  pathlib.PurePath(f).suffix.lstrip(".").lower() in extensions
+                  or (
+                      not pathlib.PurePath(f).suffix
+                      and pathlib.PurePath(f).name.startswith(".")
+                      and pathlib.PurePath(f).name.lstrip(".").lower() in extensions
+                  )
+                  or (
+                      not pathlib.PurePath(f).suffix
+                      and not pathlib.PurePath(f).name.startswith(".")
+                      and config.get("default_extension") in extensions
+                  )
+              )
+          ]
+
+          pathlib.Path("changed-files.txt").write_text(
+              "\n".join(files) + ("\n" if files else "")
+          )
+          PY
+
+      - name: Restore lychee cache
+        id: restore-cache
+        uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+        with:
+          path: .lycheecache
+          key: cache-lychee-${{ github.sha }}
+          restore-keys: cache-lychee-
+
+      - name: Run lychee
+        uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
+        with:
+          # PAT with read-only permissions used to avoid GitHub rate limits (allows 5,000 requests per hour)
+          # Won't be available to forks in pull requests due to restrictions, so falls back to default
+          # GITHUB_TOKEN with 1,000 requests per hour instead.
+          token: ${{ secrets.LYCHEE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+
+          fail: true
+          failIfEmpty: false # Avoid failing in PRs with no links in changed files (`true` is the default)
+
+          # Config is loaded from lychee.toml
+          args: >
+            ${{ github.event_name == 'pull_request' && '--files-from changed-files.txt' || '.' }}
+
+      - name: Save lychee cache
+        uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+        if: always()
+        with:
+          path: .lycheecache
+          key: ${{ steps.restore-cache.outputs.cache-primary-key }}

+ 54 - 0
.lycheeignore

@@ -0,0 +1,54 @@
+# URLs used as examples
+^https://codeload\.github\.com/FreshRSS/FreshRSS/zip/x\.y\.z
+^https://[^/]+\.domain/
+^http://wiremock:8080/
+^http://freshrss/
+
+# URLs used in source code / tests
+^http://www\.blogger\.com/feeds/1/posts
+^https://www\.blogger\.com/feeds/1/posts
+^http://a\.bc/
+^https://web\.archive\.org/save/~LINK~
+^https://www\.facebook\.com/sharer\.php\?u=~LINK~&amp;t=~TITLE~
+^https://archive\.ph/submit/\?url=~LINK~
+^https://archive\.is/submit/\?url=~LINK~
+^https://www\.reddit\.com/submit\?url=~LINK~
+^https://publish\.buffer\.com/compose\?url=~LINK~&amp;text=~TITLE
+^https://www\.journalduhacker\.net/stories/new\?url=~LINK~&amp;title=~TITLE~
+^https://www\.linkedin\.com/shareArticle\?url=~LINK~&amp;title=~TITLE~&amp;source=FreshRSS
+^https://www\.xing\.com/spi/shares/new\?url=~LINK
+
+# URLs that likely exist, but will be published in the future
+^https://github\.com/FreshRSS/FreshRSS/security/advisories/.*
+
+# Namespaces
+^http://purl\.org/dc/elements/1\.1/
+^http://search\.yahoo\.com/mrss/
+^http://www\.w3\.org/2005/Atom
+^https://freshrss\.org/opml
+
+# Old references (HTTP)
+^http://freshrss\.org/
+
+# Wrongly returned status codes
+^https://www\.pikapods\.com/pods\?run=freshrss
+
+# Unreachable due to bot protection
+^https://stackoverflow\.com/
+^https://serverfault\.com/
+^https://docs\.slackware\.com/
+^https://bugs\.mysql\.com/
+^https://dev\.mysql\.com/
+^https://alternativeto\.net/
+^https://www\.patreon\.com/
+^https://www\.reddit\.com/
+
+# Works normally in the browser, but not in lychee
+^https://www\.vsta\.org/andy/
+^https://gnusocial\.rocks/
+^https://friendi\.ca/
+
+# TODO: Dead?
+^https://template\.run\.claw\.cloud/\?openapp=system-fastdeploy%3FtemplateName%3Dfreshrss
+^https://amaury\.carrade\.eu/
+^https://www\.pimsnel\.com/

+ 44 - 0
lychee.toml

@@ -0,0 +1,44 @@
+# https://lychee.cli.rs/guides/config/
+
+verbose = "warn"
+no_progress = true
+cache = true
+max_cache_age = "1d"
+
+accept = "200..=204, 302, 307, 429"
+cache_exclude_status = "500..=599, 403, 429"
+
+require_https = true
+exclude_all_private = true
+
+max_concurrency = 50
+accept_timeouts = false # Retry instead
+timeout = 15
+max_retries = 3
+retry_wait_time = 1
+
+extensions = [
+    "md", "html", "css", "js", "php", "phtml", "sh", "json", "jsonc", "xml", "yml", "yaml", "toml", "neon", "dist", "conf", "txt",
+
+    # Dotfiles
+    "htaccess", "editorconfig", "gitattributes", "gitignore", "jshintignore", "jshintrc", "stylelintignore", "dockerignore"
+]
+
+# For extensionless files
+default_extension = "txt"
+
+include_verbatim = true
+hidden = true
+
+exclude_path = [
+  # Files
+  '(^|/)index\.html$',
+  '(^|/)\.github/ISSUE_TEMPLATE/feed-report-form\.yml$',
+
+  # Directories
+  '(^|/)data/(cache|users|favicons)($|/)',
+  '(^|/)lib/(phpmailer|simplepie|phpgt|marienfressinaud)($|/)',
+  '(^|/)docs/(_includes|_layouts|i18n)($|/)',
+  '(^|/)p/scripts/vendor($|/)'
+]
+