Skip to content

Replace issue-triggered plugin sync with scheduled pull from the plugins registry #2299

Replace issue-triggered plugin sync with scheduled pull from the plugins registry

Replace issue-triggered plugin sync with scheduled pull from the plugins registry #2299

Workflow file for this run

name: Link Check PR Changes
on:
pull_request:
paths:
- 'content/**/*.md'
- 'data/**/*.yml'
- 'layouts/**/*.html'
types: [opened, synchronize, reopened]
jobs:
link-check:
name: Check links in affected files
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Detect content changes
id: detect
run: |
echo "🔍 Detecting changes between ${{ github.base_ref }} and ${{ github.sha }}"
# For PRs, use the GitHub Files API to get changed files
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "Using GitHub API to detect PR changes..."
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \
| jq -r '.[].filename' > all_changed_files.txt
else
echo "Using git diff to detect changes..."
git diff --name-only ${{ github.event.before }}..${{ github.sha }} > all_changed_files.txt
fi
# Filter for content markdown files
CHANGED_FILES=$(grep '^content/.*\.md$' all_changed_files.txt || true)
# When data/products.yml changes, the rendered download/install pages
# change their dl.influxdata.com URLs (built from {{< latest-patch >}})
# even though no content/*.md file changed — so they'd otherwise be
# skipped. Add them here so a bad or stale download URL is still caught.
# The map step runs with --existing-only, so any path that doesn't
# exist is silently ignored.
#
# Detect the change with git diff over the full PR range, not the
# Files API list above (which is unpaginated and caps at 30 files, so
# data/products.yml could be missed on a large PR).
if git diff --name-only \
"${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" \
| grep -qx 'data/products.yml'; then
DOWNLOAD_PAGES="content/influxdb3/core/install.md
content/influxdb3/enterprise/install/_index.md
content/influxdb3/explorer/install.md
content/telegraf/v1/install.md
content/influxdb/v2/install/_index.md
content/influxdb/v1/introduction/install.md
content/chronograf/v1/introduction/installation.md
content/enterprise_influxdb/v1/introduction/installation/single-server.md"
echo "🔽 data/products.yml changed — adding download/install pages to the link check"
CHANGED_FILES=$(printf '%s\n%s\n' "$CHANGED_FILES" "$DOWNLOAD_PAGES" \
| sed 's/^[[:space:]]*//' | grep -v '^$' | sort -u)
fi
echo "📁 All changed files:"
cat all_changed_files.txt
echo ""
echo "📝 Content markdown files:"
echo "$CHANGED_FILES"
if [[ -n "$CHANGED_FILES" ]]; then
echo "✅ Found $(echo "$CHANGED_FILES" | wc -l) changed content file(s)"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "changed-content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Check if any shared content files were modified
SHARED_CHANGES=$(echo "$CHANGED_FILES" | grep '^content/shared/' || true)
if [[ -n "$SHARED_CHANGES" ]]; then
echo "has-shared-content=true" >> $GITHUB_OUTPUT
echo "🔄 Detected shared content changes: $SHARED_CHANGES"
else
echo "has-shared-content=false" >> $GITHUB_OUTPUT
fi
else
echo "❌ No content changes detected"
echo "has-changes=false" >> $GITHUB_OUTPUT
echo "has-shared-content=false" >> $GITHUB_OUTPUT
fi
- name: Skip if no content changes
if: steps.detect.outputs.has-changes == 'false'
run: |
echo "No content changes detected in this PR - skipping link check"
echo "✅ **No content changes detected** - link check skipped" >> $GITHUB_STEP_SUMMARY
- name: Validate support links
if: steps.detect.outputs.has-changes == 'true'
run: |
echo "${{ steps.detect.outputs.changed-content }}" | tr ' ' '\n' | \
xargs -r .ci/scripts/check-support-links.sh
- name: Validate source frontmatter paths
if: steps.detect.outputs.has-changes == 'true'
run: |
echo "${{ steps.detect.outputs.changed-content }}" | tr ' ' '\n' | \
xargs -r .ci/scripts/check-source-paths.sh
- name: Setup Node.js
if: steps.detect.outputs.has-changes == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: 'yarn'
- name: Install dependencies
if: steps.detect.outputs.has-changes == 'true'
run: yarn install --frozen-lockfile
- name: Build Hugo site
if: steps.detect.outputs.has-changes == 'true'
run: npx hugo --minify
- name: Install lychee
if: steps.detect.outputs.has-changes == 'true'
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
# Skip actual link checking - we just want to install lychee for link-checker to use
args: --version
# Don't fail if version check fails - we just need lychee installed
fail: false
- name: Download link-checker binary
if: steps.detect.outputs.has-changes == 'true'
run: |
echo "Downloading link-checker binary from docs-v2 releases..."
# Download from docs-v2's own releases (always accessible)
curl -L -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-o link-checker-info.json \
"https://api.github.com/repos/influxdata/docs-v2/releases/tags/link-checker-v1.5.0"
# Extract download URL for linux binary
DOWNLOAD_URL=$(jq -r '.assets[] | select(.name | test("link-checker.*linux")) | .url' link-checker-info.json)
if [[ "$DOWNLOAD_URL" == "null" || -z "$DOWNLOAD_URL" ]]; then
echo "❌ No linux binary found in release"
echo "Available assets:"
jq -r '.assets[].name' link-checker-info.json
exit 1
fi
echo "📥 Downloading: $DOWNLOAD_URL"
curl -L -H "Accept: application/octet-stream" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-o link-checker "$DOWNLOAD_URL"
chmod +x link-checker
./link-checker --version
- name: Verify link checker config exists
if: steps.detect.outputs.has-changes == 'true'
run: |
if [[ ! -f .ci/link-checker/production.lycherc.toml ]]; then
echo "❌ Configuration file .ci/link-checker/production.lycherc.toml not found"
echo "Please copy production.lycherc.toml from docs-tooling/link-checker/"
exit 1
fi
echo "✅ Using configuration: .ci/link-checker/production.lycherc.toml"
- name: Fetch known link issues
# Open issues labeled area:links; broken URLs matching one are
# downgraded to warnings so CI passes. Read-only API call, safe
# on fork PRs. On failure the classify step treats the list as
# empty and behavior is unchanged.
if: steps.detect.outputs.has-changes == 'true'
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { writeKnownLinkIssues } =
await import('${{ github.workspace }}/.github/scripts/fetch-known-link-issues.js');
const count = await writeKnownLinkIssues(github, context,
'${{ github.workspace }}/known-link-issues.json');
core.info(`Loaded ${count} open area:links issue(s)`);
- name: Map changed content to public files
if: steps.detect.outputs.has-changes == 'true'
id: mapping
run: |
echo "Mapping changed content files to public HTML files..."
# Create temporary file with changed content files
echo "${{ steps.detect.outputs.changed-content }}" > changed-files.txt
# Map content files to public files
PUBLIC_FILES=$(cat changed-files.txt | xargs -r ./link-checker map --existing-only)
if [[ -n "$PUBLIC_FILES" ]]; then
echo "Found affected public files:"
echo "$PUBLIC_FILES"
echo "public-files<<EOF" >> $GITHUB_OUTPUT
echo "$PUBLIC_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Count files for summary
FILE_COUNT=$(echo "$PUBLIC_FILES" | wc -l)
echo "file-count=$FILE_COUNT" >> $GITHUB_OUTPUT
else
echo "No public files found to check"
echo "public-files=" >> $GITHUB_OUTPUT
echo "file-count=0" >> $GITHUB_OUTPUT
fi
- name: Run link checker
if: steps.detect.outputs.has-changes == 'true' && steps.mapping.outputs.public-files != ''
id: link-check
run: |
echo "Checking links in ${{ steps.mapping.outputs.file-count }} affected files..."
# Create temporary file with public files list
echo "${{ steps.mapping.outputs.public-files }}" > public-files.txt
# Run link checker with detailed JSON output
set +e # Don't fail immediately on error
cat public-files.txt | xargs -r ./link-checker check \
--config .ci/link-checker/production.lycherc.toml \
--format json \
--output link-check-results.json
EXIT_CODE=$?
if [[ -f link-check-results.json ]]; then
# Parse results using actual v1.3.1 JSON structure
ERROR_COUNT=$(jq -r '.summary.error_count // 0' link-check-results.json)
WARNING_COUNT=$(jq -r '.summary.warning_count // 0' link-check-results.json)
TOTAL_COUNT=$(jq -r '.summary.total_checked // 0' link-check-results.json)
SUCCESS_RATE=$(jq -r '.summary.success_rate // 0' link-check-results.json)
# Reclassify file-not-found warnings as errors, and downgrade
# errors matching open area:links issues (known issues) back to
# warnings so tracked, already-reported links don't fail CI.
node .github/scripts/link-check-classify.js \
link-check-results.json known-link-issues.json
ERROR_COUNT=$(jq -r '.summary.error_count // 0' link-check-results.json)
WARNING_COUNT=$(jq -r '.summary.warning_count // 0' link-check-results.json)
KNOWN_COUNT=$(jq -r '.summary.known_issue_count // 0' link-check-results.json)
echo "error-count=$ERROR_COUNT" >> $GITHUB_OUTPUT
echo "warning-count=$WARNING_COUNT" >> $GITHUB_OUTPUT
echo "known-count=$KNOWN_COUNT" >> $GITHUB_OUTPUT
echo "total-count=$TOTAL_COUNT" >> $GITHUB_OUTPUT
echo "success-rate=$SUCCESS_RATE" >> $GITHUB_OUTPUT
if [[ $ERROR_COUNT -gt 0 ]]; then
echo "❌ Found $ERROR_COUNT broken links out of $TOTAL_COUNT total links"
echo "check-result=failed" >> $GITHUB_OUTPUT
else
echo "✅ All $TOTAL_COUNT links are valid ($WARNING_COUNT warnings, $KNOWN_COUNT known issue(s))"
echo "check-result=passed" >> $GITHUB_OUTPUT
fi
# Pass/fail is driven by the classified error count (post-downgrade),
# not the checker's raw exit code, so a known-issue downgrade can
# actually let CI pass.
if [[ $ERROR_COUNT -gt 0 ]]; then
exit 1
else
exit 0
fi
else
echo "❌ Link check failed to generate results"
echo "check-result=error" >> $GITHUB_OUTPUT
exit $EXIT_CODE
fi
- name: Process and report results
if: always() && steps.detect.outputs.has-changes == 'true' && steps.mapping.outputs.public-files != ''
env:
FILE_COUNT: ${{ steps.mapping.outputs.file-count }}
TOTAL_COUNT: ${{ steps.link-check.outputs.total-count }}
ERROR_COUNT: ${{ steps.link-check.outputs.error-count }}
WARNING_COUNT: ${{ steps.link-check.outputs.warning-count }}
KNOWN_COUNT: ${{ steps.link-check.outputs.known-count }}
SUCCESS_RATE: ${{ steps.link-check.outputs.success-rate }}
CHECK_RESULT: ${{ steps.link-check.outputs.check-result }}
run: |
if [[ -f link-check-results.json ]]; then
# Generate summary header
echo "## Link Check Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Files Checked | ${FILE_COUNT} |" >> $GITHUB_STEP_SUMMARY
echo "| Total Links | ${TOTAL_COUNT} |" >> $GITHUB_STEP_SUMMARY
echo "| Errors | ${ERROR_COUNT} |" >> $GITHUB_STEP_SUMMARY
echo "| Warnings | ${WARNING_COUNT} |" >> $GITHUB_STEP_SUMMARY
if [[ "${KNOWN_COUNT}" -gt 0 ]]; then
echo "| Known Issues | ${KNOWN_COUNT} |" >> $GITHUB_STEP_SUMMARY
fi
echo "| Success Rate | ${SUCCESS_RATE}% |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Report broken links (errors) with annotations
if [[ "${CHECK_RESULT}" == "failed" ]]; then
echo "### Broken Links" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | Source File | Broken URL | Error |" >> $GITHUB_STEP_SUMMARY
echo "|----------|------------|------------|-------|" >> $GITHUB_STEP_SUMMARY
# Process errors (these fail CI)
jq -c '.errors[]?' link-check-results.json 2>/dev/null | while read -r entry; do
URL=$(echo "$entry" | jq -r '.url // "unknown"')
ERROR=$(echo "$entry" | jq -r '.error // "Unknown error"')
FILE=$(echo "$entry" | jq -r '.file // "unknown"')
LINE=$(echo "$entry" | jq -r '.line // empty')
# Map public path to content path for annotations
CONTENT_FILE=$(echo "$FILE" | sed 's|.*/public/|content/|' | sed 's|/index\.html$|/_index.md|')
# Create GitHub annotation
if [[ -n "$LINE" && "$LINE" != "null" ]]; then
echo "::error file=${CONTENT_FILE},line=${LINE}::Broken link: ${URL} (${ERROR})"
else
echo "::error file=${CONTENT_FILE}::Broken link: ${URL} (${ERROR})"
fi
# Add row to summary table
SAFE_URL=$(echo "$URL" | sed 's/|/\\|/g')
SAFE_ERROR=$(echo "$ERROR" | sed 's/|/\\|/g' | cut -c1-80)
echo "| 🔴 error | \`${CONTENT_FILE}\` | ${SAFE_URL} | ${SAFE_ERROR} |" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "❌ **Link check failed** — fix the broken links listed above before merging." >> $GITHUB_STEP_SUMMARY
else
echo "✅ **All links are valid**" >> $GITHUB_STEP_SUMMARY
fi
# Report known issues (downgraded from errors; don't fail CI)
KNOWN_ARRAY_LEN=$(jq '[.warnings[]? | select(.knownIssue)] | length' link-check-results.json 2>/dev/null || echo 0)
if [[ "$KNOWN_ARRAY_LEN" -gt 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Known Issues (downgraded — do not fail CI)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Source File | URL | Error | Known Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------------|-----|-------|-------------|" >> $GITHUB_STEP_SUMMARY
jq -c '.warnings[]? | select(.knownIssue)' link-check-results.json 2>/dev/null | while read -r entry; do
URL=$(echo "$entry" | jq -r '.url // "unknown"')
ERROR=$(echo "$entry" | jq -r '.error // "Unknown"')
FILE=$(echo "$entry" | jq -r '.file // "unknown"')
ISSUE_NUM=$(echo "$entry" | jq -r '.knownIssue.number')
CONTENT_FILE=$(echo "$FILE" | sed 's|.*/public/|content/|' | sed 's|/index\.html$|/_index.md|')
SAFE_URL=$(echo "$URL" | sed 's/|/\\|/g')
SAFE_ERROR=$(echo "$ERROR" | sed 's/|/\\|/g' | cut -c1-80)
echo "::notice file=${CONTENT_FILE}::Known broken link (issue #${ISSUE_NUM}): ${URL}"
echo "| \`${CONTENT_FILE}\` | ${SAFE_URL} | ${SAFE_ERROR} | [#${ISSUE_NUM}](https://github.com/${{ github.repository }}/issues/${ISSUE_NUM}) |" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "_These broken links match open \`area:links\` issues and were downgraded to warnings so CI passes. Fix them via the linked issues._" >> $GITHUB_STEP_SUMMARY
fi
# Report warnings (don't fail CI, but useful context)
WARNING_ARRAY_LEN=$(jq '[.warnings[]? | select(.knownIssue | not)] | length' link-check-results.json 2>/dev/null || echo 0)
if [[ "$WARNING_ARRAY_LEN" -gt 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>⚠️ ${WARNING_ARRAY_LEN} warning(s) (do not fail CI)</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Source File | URL | Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------------|-----|-------|" >> $GITHUB_STEP_SUMMARY
jq -c '.warnings[]? | select(.knownIssue | not)' link-check-results.json 2>/dev/null | head -20 | while read -r entry; do
URL=$(echo "$entry" | jq -r '.url // "unknown"')
ERROR=$(echo "$entry" | jq -r '.error // "Unknown"')
FILE=$(echo "$entry" | jq -r '.file // "unknown"')
CONTENT_FILE=$(echo "$FILE" | sed 's|.*/public/|content/|' | sed 's|/index\.html$|/_index.md|')
SAFE_URL=$(echo "$URL" | sed 's/|/\\|/g')
SAFE_ERROR=$(echo "$ERROR" | sed 's/|/\\|/g' | cut -c1-80)
echo "| \`${CONTENT_FILE}\` | ${SAFE_URL} | ${SAFE_ERROR} |" >> $GITHUB_STEP_SUMMARY
done
if [[ "$WARNING_ARRAY_LEN" -gt 20 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "_Showing first 20 of ${WARNING_ARRAY_LEN} warnings. Download the artifact for full results._" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi
# Add helpful tips
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>💡 Troubleshooting Tips</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **404 errors**: The linked page doesn't exist. Check for typos or update the link." >> $GITHUB_STEP_SUMMARY
echo "- **Relative links**: Use relative paths starting with \`/\` for internal links." >> $GITHUB_STEP_SUMMARY
echo "- **Anchors**: Ensure heading anchors match the linked fragment exactly." >> $GITHUB_STEP_SUMMARY
echo "- **Warnings**: External sites may be temporarily unavailable — these don't fail CI." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ **Link check could not complete** — no results file generated" >> $GITHUB_STEP_SUMMARY
fi
- name: Post PR comment
# Sticky comment mirroring the step summary so results are visible
# on the PR page. Only created when problems are found; a passing
# run updates an existing comment to the success state.
# Fork PRs get a read-only GITHUB_TOKEN, so skip commenting there —
# the step summary and annotations remain the fallback.
if: >-
always() &&
steps.detect.outputs.has-changes == 'true' &&
steps.mapping.outputs.public-files != '' &&
github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
FILE_COUNT: ${{ steps.mapping.outputs.file-count }}
TOTAL_COUNT: ${{ steps.link-check.outputs.total-count }}
ERROR_COUNT: ${{ steps.link-check.outputs.error-count }}
WARNING_COUNT: ${{ steps.link-check.outputs.warning-count }}
SUCCESS_RATE: ${{ steps.link-check.outputs.success-rate }}
CHECK_RESULT: ${{ steps.link-check.outputs.check-result }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
script: |
const { loadResults, upsertLinkCheckComment } =
await import('${{ github.workspace }}/.github/scripts/link-check-comment.js');
const results = loadResults('${{ github.workspace }}/link-check-results.json');
await upsertLinkCheckComment(github, context, {
...results,
status: process.env.CHECK_RESULT || results.status || 'error',
filesChecked: process.env.FILE_COUNT,
totalLinks: process.env.TOTAL_COUNT || results.totalLinks,
errorCount: process.env.ERROR_COUNT || results.errorCount,
warningCount: process.env.WARNING_COUNT || results.warningCount,
successRate: process.env.SUCCESS_RATE || results.successRate,
runUrl: process.env.RUN_URL,
prNumber: context.issue.number,
});
- name: Refresh stale comment when nothing was checked
# When a later commit removes the offending file, the mapping step
# finds no public files and the comment step above is skipped —
# refresh an existing failure comment instead of leaving it stale.
# updateOnly means: never create a comment on PRs that were never
# link-checked.
if: >-
always() &&
steps.detect.outputs.has-changes == 'true' &&
steps.mapping.outcome == 'success' &&
steps.mapping.outputs.public-files == '' &&
github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { upsertLinkCheckComment } =
await import('${{ github.workspace }}/.github/scripts/link-check-comment.js');
await upsertLinkCheckComment(github, context, {
status: 'skipped',
updateOnly: true,
});
- name: Upload detailed results
if: always() && steps.detect.outputs.has-changes == 'true' && steps.mapping.outputs.public-files != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: link-check-results
path: |
link-check-results.json
changed-files.txt
public-files.txt
retention-days: 30