Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ paths:
.github/workflows/**:
ignore:
- 'reusable workflow call "\$/.*" at "uses" is not following the format'
- 'specifying action "\$/actions/markdown-links" in invalid format because ref is missing'
47 changes: 47 additions & 0 deletions .github/workflows/link-check-regressions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This source file is part of the Schmiedmayer Lab open-source organization
# SPDX-FileCopyrightText: 2026 Schmiedmayer Lab and the project authors (see CONTRIBUTORS.md)
# SPDX-License-Identifier: MIT

name: Link Check Regressions

on:
pull_request:
push:
branches: [main]
schedule:
- cron: '23 9 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
live-links:
name: Bluetooth, Zenodo, and Broken-Link Detection
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Prepare isolated link fixtures
run: python3 actions/markdown-links/live_check.py prepare "$RUNNER_TEMP/link-fixture"
- name: Run the shared action, expecting the broken-link failure
id: links
continue-on-error: true
uses: $/actions/markdown-links
with:
working_directory: ${{ runner.temp }}/link-fixture
globalping_hosts: 'www.bluetooth.com,zenodo.org'
- name: Verify live results and the expected failure
if: ${{ !cancelled() }}
env:
LINK_EVIDENCE: ${{ steps.links.outputs.evidence }}
LINK_OUTCOME: ${{ steps.links.outcome }}
run: python3 actions/markdown-links/live_check.py verify "$LINK_EVIDENCE" --outcome "$LINK_OUTCOME"
- name: Retain strict live verification
if: ${{ always() && steps.links.outputs.evidence != '' }}
uses: actions/upload-artifact@v7
with:
name: live-link-verification
path: ${{ steps.links.outputs.evidence }}/live-verification.json
if-no-files-found: warn
retention-days: 7
13 changes: 9 additions & 4 deletions .github/workflows/markdown-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ on:
required: false
type: string
default: '["ubuntu-latest"]'
globalping_hosts:
description: |
Comma-separated public documentation hosts whose HTTP 403 results may be checked by Globalping.
Measurement URLs and results become public. Empty disables the fallback.
required: false
type: string
default: ''

permissions:
contents: read
Expand Down Expand Up @@ -79,8 +86,6 @@ jobs:
print(f"Private repository; ignoring {pattern}")
PY
- name: Linkspector
uses: umbrelladocs/action-linkspector@v1
uses: $/actions/markdown-links
with:
filter_mode: nofilter
reporter: github-pr-annotations
fail_level: error
globalping_hosts: ${{ inputs.globalping_hosts }}
10 changes: 9 additions & 1 deletion .github/workflows/repository-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ on:
required: false
type: string
default: '["ubuntu-latest"]'
globalping_hosts:
description: |
Comma-separated public documentation hosts permitted for the optional Globalping HTTP 403 fallback.
Measurement URLs and results become public. Empty disables the fallback.
required: false
type: string
default: ''

permissions:
contents: read
Expand All @@ -35,9 +42,10 @@ jobs:

links:
name: Docs
uses: SchmiedmayerLab/.github/.github/workflows/markdown-links.yml@v0.5
uses: $/.github/workflows/markdown-links.yml
with:
runs_on_labels: ${{ inputs.runs_on_labels }}
globalping_hosts: ${{ inputs.globalping_hosts }}

actions:
name: Actions
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/self-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
steps:
- uses: actions/checkout@v7

- name: Link fallback regressions
run: python3 -m unittest discover -s actions/markdown-links -p 'test_*.py' -v

- name: The README matches the reusable workflows
run: |
python3 -c 'import yaml' 2>/dev/null || python3 -m pip install --quiet pyyaml
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@

# editors
.idea/

# Python test and action caches
__pycache__/
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<!--

This source file is part of the Schmiedmayer Lab open-source organization

Check notice on line 3 in README.md

View workflow job for this annotation

GitHub Actions / Bluetooth, Zenodo, and Broken-Link Detection

verified: https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth/ — Both probes reached the URL with verified TLS
Expand All @@ -6,7 +6,7 @@

SPDX-License-Identifier: MIT

-->

Check failure on line 9 in README.md

View workflow job for this annotation

GitHub Actions / Bluetooth, Zenodo, and Broken-Link Detection

Cannot reach does-not-exist.md Status: 404 Cannot find: does-not-exist.md

# Schmiedmayer Lab

Expand Down Expand Up @@ -162,14 +162,34 @@
##### Check Markdown Links

[`markdown-links.yml`](.github/workflows/markdown-links.yml) checks Markdown links with Linkspector.
Use it for documentation-heavy repositories. The workflow needs read access to pull requests so
Linkspector can map diagnostics to the pull request diff.
Use it for documentation-heavy repositories. Findings appear as workflow annotations, not PR comments.
It uses Linkspector's latest `0.5.x` release and the runner's installed Google Chrome. GitHub-hosted
Ubuntu runners provide Chrome; custom runners must provide it with a working browser sandbox.

Linkspector browses anonymously, so in a **private** repository every link back into that repository
— badge targets, `blob/main/LICENSE.md` — answers 404 no matter what the file tree contains. The
workflow therefore adds an ignore pattern for the repository's own URL when the repository is
private, and leaves every other link checked.

For sites that block the initial browser check, `globalping_hosts` enables a second check of
HTTP 403 results from those exact hosts. Linkspector still checks them first; this is not an
ignore list. The fallback uses [Globalping](https://globalping.io/) without an account or API key.
**Measurement URLs and results are public.** Opt in only hosts whose documentation paths may be
disclosed. HTTPS URLs with credentials, query strings, fragments, or nonstandard ports are never
submitted. Other hosts and error codes retain their original failure behavior.

The fallback asks two non-datacenter probes to GET the exact path. Two successful responses with
verified TLS resolve the finding; two 404/410 responses fail the check. Redirects, mixed responses,
blocked requests, API failures, and exhausted quotas remain **unverified warnings**. They do not
block a merge and must not be interpreted as successful verification. This verifies HTTP
reachability, not page contents or JavaScript-rendered content. The probes are community operated
and may share a network; they do not provide independent security attestations.

At most ten unique URLs are submitted per run (two probes each); repeated references share one
result. The job summary lists outcomes and measurement links, and a seven-day artifact retains
the original Linkspector report and processed results. Checker failures and unreadable reports
fail the job. Leaving `globalping_hosts` empty preserves strict Linkspector results.

```yml
jobs:
markdown-links:
Expand All @@ -178,8 +198,21 @@
permissions:
contents: read
pull-requests: read
with:
globalping_hosts: 'www.bluetooth.com,zenodo.org'
```

`repository-standards.yml` accepts and forwards the same optional input. Existing callers need
an updated shared-workflow release before enabling it. Self-repository references keep the
workflow and action at the same commit, independently of the caller's checkout.

`Link Check Regressions` runs on pull requests, pushes to `main`, and weekly. It exercises the
shared action against Bluetooth, Zenodo, a valid local document, and a deliberately missing
document. A separate live fallback check requires both public URLs to return successful
responses with verified TLS; unverified warnings fail this regression. Unit tests cover
blocked probes, outages, malformed reports, and preservation of other link errors without
depending on external services. Reports are retained for seven days; no PR comments are posted.

##### Run Periphery

[`periphery.yml`](.github/workflows/periphery.yml) runs Periphery to detect unused Swift declarations.
Expand Down
56 changes: 56 additions & 0 deletions actions/markdown-links/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# This source file is part of the Schmiedmayer Lab open-source organization
# SPDX-FileCopyrightText: 2026 Schmiedmayer Lab and the project authors (see CONTRIBUTORS.md)
# SPDX-License-Identifier: MIT

name: Check Markdown Links
description: Run Linkspector once and optionally verify public HTTP 403 results through Globalping.
inputs:
working_directory:
description: Directory containing the Markdown files and Linkspector configuration.
default: '.'
globalping_hosts:
description: Comma-separated public documentation hosts permitted for public Globalping measurements.
default: ''
outputs:
evidence:
description: Directory containing the original and processed link reports.
value: ${{ steps.check.outputs.evidence }}
runs:
using: composite
steps:
- uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false
- name: Check links
id: check
shell: bash
working-directory: ${{ inputs.working_directory }}
env:
CHECK_ACTION_PATH: ${{ github.action_path }}
GLOBALPING_HOSTS: ${{ inputs.globalping_hosts }}
run: |
set -euo pipefail
tools_dir=$(mktemp -d "$RUNNER_TEMP/link-tools.XXXXXX")
evidence_dir=$(mktemp -d "$RUNNER_TEMP/link-evidence.XXXXXX")
echo "evidence=$evidence_dir" >> "$GITHUB_OUTPUT"
npm install --prefix "$tools_dir" --ignore-scripts --no-audit --no-fund @umbrelladocs/linkspector@0.5
# The system Chrome has Ubuntu's sandbox configuration; downloaded test
# builds can be blocked by AppArmor's user-namespace restrictions.
if [ -z "${PUPPETEER_EXECUTABLE_PATH:-}" ]; then
PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome)
fi
export PUPPETEER_EXECUTABLE_PATH
"$PUPPETEER_EXECUTABLE_PATH" --version
checker_status=0
"$tools_dir/node_modules/.bin/linkspector" check --json > "$evidence_dir/linkspector.json" || checker_status=$?
python3 "$CHECK_ACTION_PATH/check_links.py" \
--report "$evidence_dir/linkspector.json" --checker-exit-code "$checker_status" \
--hosts "$GLOBALPING_HOSTS" --output "$evidence_dir/results.json"
- name: Retain link results
if: ${{ always() && steps.check.outputs.evidence != '' }}
uses: actions/upload-artifact@v7
with:
name: markdown-links-${{ github.job }}
path: ${{ steps.check.outputs.evidence }}
retention-days: 7
Loading
Loading