diff --git a/.github/workflows/link-checker.yml b/.github/workflows/link-checker.yml new file mode 100644 index 000000000..119019b07 --- /dev/null +++ b/.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 }} diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 000000000..93ff6ee20 --- /dev/null +++ b/.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~&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~&text=~TITLE +^https://www\.journalduhacker\.net/stories/new\?url=~LINK~&title=~TITLE~ +^https://www\.linkedin\.com/shareArticle\?url=~LINK~&title=~TITLE~&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/ diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 000000000..81e83a870 --- /dev/null +++ b/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($|/)' +] +