Skip to content
Closed
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
108 changes: 84 additions & 24 deletions .github/workflows/preview_create.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,31 +78,91 @@ jobs:
run: |
aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key ${PR}/index.html --body .github/workflows/resources/empty.html --content-type text/html --metadata pointer=${RUN}

# Link check runs last so it never blocks the preview deployment. It
# validates only the external links this PR adds or changes in .adoc source
# (pages or partials) — the changed link is checked once, regardless of how
# many pages include it. Non-blocking at rollout (fail: false); flip to
# fail: true once the baseline is clean.
- name: Extract external links changed in this PR
id: changed_links
run: |
base="origin/${{ github.event.pull_request.base.ref }}"
git fetch --no-tags origin "${{ github.event.pull_request.base.ref }}"
git diff "$base...HEAD" -- 'modules/**/*.adoc' \
| awk '/^\+[^+]/' \
| grep -oE 'https?://[^][<>"^ '"'"']+' \
| sed -E 's/[.,;:]+$//' | sort -u > changed-links.txt || true
count=$(wc -l < changed-links.txt | tr -d '[:space:]')
echo "count=${count:-0}" >> "$GITHUB_OUTPUT"
echo "Changed external links to validate ($count):"; cat changed-links.txt

- name: Check changed links
if: steps.changed_links.outputs.count != '0'
# Link check runs last so it can never block the preview deployment.
#
# Links are extracted from the BUILT HTML, not from the .adoc diff. lychee's
# HTML parser sees only real <a href> anchors, so URLs that appear inside
# [source] code samples (container hostnames, placeholder domains, shell and
# template literals) and Antora attributes that are substituted at build time
# can never reach the checker. Measured over 400 commits of real history,
# extracting from raw .adoc text produced a 16% false-failure rate, none of
# which was genuine link rot.
#
# The diff is still used, but only to scope the *result* to links this PR
# touched. Because it filters rendered anchors rather than feeding the
# checker, an over-broad or malformed match simply fails to intersect a real
# anchor — it cannot manufacture a failure.
- name: Check links in the built site
id: lychee
uses: lycheeverse/lychee-action@v2
with:
args: --config .lychee.toml --no-progress changed-links.txt
format: markdown
output: lychee/results.md
args: --config .lychee.toml --no-progress './build/docs/tinymce/latest/**/*.html'
format: json
output: lychee/results.json
token: ${{ secrets.GITHUB_TOKEN }}
fail: false
jobSummary: true
jobSummary: false

- name: Report links added or changed by this PR
continue-on-error: true
env:
BASE: ${{ github.event.pull_request.base.ref }}
run: |
git fetch --no-tags --quiet origin "$BASE"

# Candidate URLs from the diff. Permissive on purpose: this only scopes
# the report, so a bad match is inert rather than a false failure.
git diff "origin/${BASE}...HEAD" -- '*.adoc' \
| grep '^+' | grep -v '^+++' \
| grep -oE "https?://[^][<>\"^\`)+,|' ]+" \
| sed -E 's/[.,;:]+$//' | sort -u > touched-urls.txt || true

# Broken links found in the rendered site, as page + url + reason.
if [ ! -f lychee/results.json ]; then
echo "Link checker produced no report; skipping." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# Trim any trailing content after the JSON object: the root object is the
# only one closing at column 0, so this is an exact cut.
sed -n '1,/^}$/p' lychee/results.json > lychee/results.clean.json

jq -r '.error_map // {} | to_entries[] | .key as $page | .value[]
| [$page, .url, (.status.text // "unknown")] | @tsv' \
lychee/results.clean.json > broken-all.tsv

# Keep only the broken links this PR touched (trailing-slash tolerant).
awk -F'\t' '
function norm(u){ sub(/\/+$/,"",u); return u }
NR==FNR { touched[norm($0)]=1; next }
norm($2) in touched { print }
' touched-urls.txt broken-all.tsv > broken-mine.tsv

mine=$(wc -l < broken-mine.tsv | tr -d '[:space:]')
all=$(wc -l < broken-all.tsv | tr -d '[:space:]')

{
echo "## External link check"
echo
echo "Links changed by this pull request: $(wc -l < touched-urls.txt | tr -d '[:space:]')"
echo
if [ "$mine" -eq 0 ]; then
echo "No broken links were introduced by this pull request."
else
echo "### Broken links introduced by this pull request"
echo
echo "| Page | Link | Reason |"
echo "|------|------|--------|"
sed -E 's#^[^\t]*/docs/tinymce/latest/##' broken-mine.tsv \
| awk -F'\t' '{ printf "| %s | %s | %s |\n", $1, $2, $3 }'
fi
if [ "$all" -gt "$mine" ]; then
echo
echo "_$((all - mine)) further broken link(s) already present elsewhere in the site; these are tracked by the weekly scheduled scan._"
fi
} >> "$GITHUB_STEP_SUMMARY"

# Non-blocking at rollout. Change to `exit 1` to make this a merge gate
# once the baseline recorded in the scheduled scan is clean.
if [ "$mine" -gt 0 ]; then
echo "::warning::${mine} broken link(s) introduced by this pull request. See the job summary."
fi
8 changes: 8 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ https://malavkhatri.com/
# https://www.facebook.com/
# https://twitter.com/
# https://x.com/

# Verified by a full-site scan (2026-08-20): reachable in a browser, but the
# automated checker is refused.
# Bot detection -> 403:
https://stackoverflow.com/
https://www.graylog.org/
# MCP endpoint: rejects HEAD/GET with 405 by design, it is not a web page.
https://tinymcedocs.mcp.kapa.ai/
42 changes: 42 additions & 0 deletions modules/ROOT/pages/tinydoc-3514-link-check-proof.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
= Link check proof page
:navtitle: Link check proof
:description: Temporary page used to verify the automated external-link check. Not for merge.
:keywords: linkcheck

This page exists only to exercise the automated external-link check. It is not
part of the documentation and must not be merged.

== Links that should be reported as broken

A dead page on a live host: https://developer.mozilla.org/en-US/docs/Web/API/TinyDoc3514DoesNotExist[a page that does not exist].

== Links that should pass

A live specification: https://www.w3.org/TR/wai-aria-1.2/

A live page in macro form: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage[postMessage].

== Links that should be ignored

An internal cross-reference: xref:events.adoc[Events].

A link to this site: https://www.tiny.cloud/docs/tinymce/latest/

A host that refuses automated checkers: https://www.npmjs.com/package/tinymce[the npm package].

== Links that must never reach the checker

Every URL below sits inside a code sample. None of them is a real anchor, so
none should be checked or reported.

[source,js]
----
tinymce.init({
selector: 'textarea',
ai_request_url: 'http://ai-service:8000/v1/chat/completions',
fallback_url: 'https://ai.yourcompany.com/v1/conversations',
cdn_url: 'https://cdn.tiny.cloud/1/no-api-key/tinymce/{productmajorversion}/tinymce.min.js',
docker_url: 'http://host.docker.internal:11434/v1',
dead_url: 'https://developer.mozilla.org/en-US/docs/Web/API/AlsoDoesNotExistTinyDoc3514'
});
----
Loading