ci: add scheduled audit-docs-paths workflow (#27245)

## What

Adds `.github/workflows/audit-docs-paths.yaml`, a scheduled workflow
that runs the docs-URL drift audit (`site/scripts/audit-docs-paths.mjs`,
added in #25740) on a weekly cron and on demand, so drift is caught
automatically instead of only when someone runs the script by hand.
Scheduling was suggested by @bpmct on #25740.

## How it works

- **Triggers:** `schedule` (weekly, Monday 09:00 UTC — same cadence as
`weekly-docs`) and `workflow_dispatch`.
- **Checks out both repos:** `coder/coder` (root) and `coder/coder.com`
(into `coder.com/`, read with the `cdrci` CI-bot token). The audit
covers references in both repos.
- **Runs the audit** with absolute `--roots` (required, otherwise the
report can't classify findings by repo).
- **Always** uploads the dated report as the `audit-docs-paths-report`
artifact and writes it to the run summary.
- **On findings:** opens or updates a single deduplicated tracked issue
with the report, and fails the run (red check). **On a clean run:**
closes that issue.

## Enabling (dormant until then)

The audit reads **coder/coder.com, a private repo**, which the default
`GITHUB_TOKEN` can't read, so the coder.com checkout uses the existing
**`cdrci`** CI-bot token (`secrets.CDRCI_GITHUB_TOKEN`) — already used
for cross-repo checkouts in `release.yaml`/`tag-and-release.yaml`, and
`cdrci` is a coder.com collaborator (verified). No new App to stand up.
The job is gated behind `vars.AUDIT_DOCS_PATHS_ENABLED` so it merges
dormant and can be validated before going live.

To turn it on:
1. Set `vars.AUDIT_DOCS_PATHS_ENABLED = 'true'`.
2. Run once via `workflow_dispatch` to confirm the end-to-end run.

## Also in this PR

Removes the dormant `audit-docs-paths` job embedded in
`weekly-docs.yaml` (added in #25740, gated off pending the same
credential). The new dedicated workflow supersedes it; the
`weekly-docs.yaml` diff is exactly that job removal.

## Validation

- `actionlint -shellcheck= -ignore set-output` passes locally; PR
`title`, `lint-actions`, and `lint-docs` are green.
- **Credential check:** `cdrci` is a collaborator on coder/coder.com
(read access confirmed); `secrets.CDRCI_GITHUB_TOKEN` already exists in
this repo. (Note: `cdrci2` is *not* a coder.com collaborator, so an
earlier `CDRCI2_` attempt was corrected to `CDRCI_`.)
- **Pre-flight audit against current `main` (both repos): 0 findings** —
148 `/docs/*` redirect rules indexed; 1846 coder/coder + 432 coder.com
TS/TSX files scanned. So a `workflow_dispatch` on `main` today passes
green with no issue filed (the "empty audit succeeds" criterion). The
failure path can be checked by injecting a stale path on a throwaway
branch.

## Decisions for review

- **Mechanism** = tracked issue + failed check + artifact ("both" from
the issue). Easy to narrow to issue-only or fail-only.
- Reused `AUDIT_DOCS_PATHS_ENABLED` and removed the embedded job rather
than adding a second gate.
- Named the file `.yaml` to match the repo's other docs workflows (the
issue text said `.yml`).

Linear: https://linear.app/codercom/issue/DOCS-366
This commit is contained in:
Nick Vigilante
2026-08-10 13:40:44 -04:00
committed by GitHub
parent ad100452d4
commit 2f34e1abd0
2 changed files with 194 additions and 69 deletions
+194
View File
@@ -0,0 +1,194 @@
name: audit-docs-paths
# Scheduled audit for docs-URL drift.
#
# site/scripts/audit-docs-paths.mjs cross-references every docs("...")
# call, every hardcoded coder.com/docs/... URL, and every ](/docs/...)
# Markdown link in the TS/TSX of BOTH coder/coder (site/src) and
# coder/coder.com (src) against the source side of every /docs/* rule in
# coder/coder.com/redirects.json. Anything that matches a redirect source
# is stale: the reference points at a path that now redirects elsewhere
# and should be updated to the redirect's destination.
#
# This runs the audit on a weekly cron (and on demand) so drift is caught
# automatically instead of only when someone remembers to run the script.
# The suggestion to schedule it came from the audit script's original PR,
# coder/coder#25740.
#
# Why this job is gated (vars.AUDIT_DOCS_PATHS_ENABLED)
# ----------------------------------------------------
# The audit needs coder/coder.com, which is a PRIVATE repo. The default
# GITHUB_TOKEN cannot read another repo, so the coder.com checkout below
# uses the cdrci machine-user token (secrets.CDRCI_GITHUB_TOKEN), the
# same org CI credential already used for cross-repo checkouts in
# release.yaml. The job
# stays dormant behind a repository variable so it can merge without
# running until an operator has validated it once.
#
# To enable
# 1. secrets.CDRCI_GITHUB_TOKEN (the cdrci CI bot, a coder.com
# collaborator) reads coder/coder.com. Issue writes on this repo are
# covered by the default GITHUB_TOKEN below, not by this token.
# 2. Set the repository variable AUDIT_DOCS_PATHS_ENABLED to 'true'.
# 3. Trigger once from the Actions UI (workflow_dispatch) to confirm an
# end-to-end run before relying on the schedule.
#
# What it does with findings
# * Always uploads the dated report as a build artifact and writes it to
# the run summary, so a report exists even when the audit is clean.
# * On findings, opens (or updates) a single deduplicated tracked issue
# with the report and fails the run so the check goes red.
# * On a clean run, closes the tracked issue if one is open.
on:
schedule:
# Weekly, Monday 09:00 UTC - same cadence as the weekly-docs link check.
- cron: "0 9 * * 1"
workflow_dispatch: # manual, for testing and on-demand audits
permissions:
contents: read
concurrency:
group: audit-docs-paths
cancel-in-progress: false
jobs:
audit-docs-paths:
# Dormant until an operator flips the variable. See the header comment.
if: vars.AUDIT_DOCS_PATHS_ENABLED == 'true'
runs-on: ubuntu-latest
permissions:
contents: read # checkout
issues: write # open/update/close the tracked issue via GITHUB_TOKEN
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
# coder/coder at the workspace root so the local composite actions
# under ./.github/actions/* resolve normally.
- name: Checkout coder/coder
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# coder/coder.com beside it, read with the cdrci org CI token
# (the default GITHUB_TOKEN can't read another private repo). The
# audit's report path detection keys off the ".../coder/site/" and
# ".../coder.com/src/" path segments, which the absolute --roots
# below preserve.
- name: Checkout coder/coder.com
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ github.repository_owner }}/coder.com
path: coder.com
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
persist-credentials: false
- name: Set up mise tools
uses: ./.github/actions/setup-mise
with:
install-args: "node"
- name: Run docs-paths audit
id: audit
env:
WORKSPACE: ${{ github.workspace }}
run: |
set -euo pipefail
# Absolute --roots are required: the audit classifies findings by
# matching the "/coder/site/" and "/coder.com/src/" segments in
# each file's absolute path. Relative roots would leave every
# finding unclassified.
node site/scripts/audit-docs-paths.mjs \
--redirects="${WORKSPACE}/coder.com/redirects.json" \
--roots="${WORKSPACE}/site/src,${WORKSPACE}/coder.com/src" \
--out="${WORKSPACE}/audit-report.md" \
2>&1 | tee "${WORKSPACE}/audit-output.txt"
count=$(grep -oP 'Total findings: \K\d+' "${WORKSPACE}/audit-output.txt" || echo "0")
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "Total findings: ${count}"
{
echo "# audit-docs-paths"
echo
echo "Findings: **${count}**"
echo
cat "${WORKSPACE}/audit-report.md"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload audit report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: audit-docs-paths-report
path: ${{ github.workspace }}/audit-report.md
if-no-files-found: warn
- name: Open, update, or close tracked issue
if: always() && steps.audit.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
COUNT: ${{ steps.audit.outputs.count }}
REPORT: ${{ github.workspace }}/audit-report.md
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TITLE="Docs URL drift detected by audit-docs-paths"
MARKER="<!-- audit-docs-paths-tracked-issue -->"
# Find an existing open tracked issue by exact title.
existing=$(gh issue list --repo "$REPO" --state open \
--search "${TITLE} in:title" \
--json number,title \
--jq "map(select(.title == \"${TITLE}\")) | (.[0].number // empty)")
if [ "${COUNT:-0}" -gt 0 ]; then
body_file="$(mktemp)"
{
echo "${MARKER}"
echo
echo "The scheduled \`audit-docs-paths\` workflow found **${COUNT}** stale docs path reference(s) that resolve to a redirect source in \`coder/coder.com/redirects.json\`. Each should be updated to the redirect's destination."
echo
echo "- Latest run: ${RUN_URL}"
echo "- Full report: the \`audit-docs-paths-report\` artifact on that run."
echo
echo "<details><summary>Report</summary>"
echo
# Cap the embedded report to stay under GitHub's ~65k issue
# body limit; the full report is always in the artifact.
head -c 55000 "${REPORT}"
echo
echo "</details>"
} > "${body_file}"
if [ -n "${existing}" ]; then
gh issue edit "${existing}" --repo "${REPO}" --body-file "${body_file}"
echo "Updated tracked issue #${existing}."
else
gh issue create --repo "${REPO}" --title "${TITLE}" --body-file "${body_file}"
echo "Created tracked issue."
fi
else
if [ -n "${existing}" ]; then
gh issue comment "${existing}" --repo "${REPO}" \
--body "Audit is clean as of [this run](${RUN_URL}). Closing."
gh issue close "${existing}" --repo "${REPO}"
echo "Closed tracked issue #${existing} (audit clean)."
else
echo "Audit clean and no tracked issue open; nothing to do."
fi
fi
- name: Fail the run when findings exist
if: steps.audit.outputs.count != '' && steps.audit.outputs.count != '0'
env:
COUNT: ${{ steps.audit.outputs.count }}
run: |
echo "::error::audit-docs-paths found ${COUNT} stale docs path reference(s). See the tracked issue and the audit-docs-paths-report artifact."
exit 1
-69
View File
@@ -135,72 +135,3 @@ jobs:
echo "Sent Slack notification"
env:
LOGS_URL: https://github.com/coder/coder/actions/runs/${{ github.run_id }}
audit-docs-paths:
# Disabled by default: this audit fetches a config file from a private
# upstream source and the workflow currently lacks the credentials to
# read it. Pending provisioning of a dedicated GitHub App with the
# required cross-repo Contents: Read. To re-enable once the App
# credentials are in place, set the repository variable
# AUDIT_DOCS_PATHS_ENABLED to 'true'.
if: vars.AUDIT_DOCS_PATHS_ENABLED == 'true'
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for audit script
id: check-script
run: |
if [ ! -f site/scripts/audit-docs-paths.mjs ]; then
echo "::notice::Audit script not yet available (pending PR #25740). Skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up mise tools
if: steps.check-script.outputs.skip != 'true'
uses: ./.github/actions/setup-mise
with:
install-args: "node"
- name: Fetch redirects.json
if: steps.check-script.outputs.skip != 'true'
run: |
curl -sfL \
https://raw.githubusercontent.com/coder/coder.com/refs/heads/main/redirects.json \
-o /tmp/redirects.json
- name: Audit TS/TSX docs paths against redirects
if: steps.check-script.outputs.skip != 'true'
run: |
node site/scripts/audit-docs-paths.mjs \
--redirects=/tmp/redirects.json \
--roots=site/src \
--out=/tmp/audit-report.md 2>&1 | tee /tmp/audit-output.txt
count=$(grep -oP 'Total findings: \K\d+' /tmp/audit-output.txt || echo "0")
if [ "$count" -gt 0 ]; then
echo "::error::Found $count stale docs path(s) pointing at redirect sources"
cat /tmp/audit-report.md >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: Send Slack notification
if: failure() && github.event_name != 'pull_request'
run: |
curl \
-X POST \
-H 'Content-type: application/json' \
-d '{"text":":warning: *Stale docs paths found in site/src/.*\nTS/TSX files reference docs URLs that now redirect. Please check the logs: '"${LOGS_URL}"'"}' "${{ secrets.DOCS_LINK_SLACK_WEBHOOK }}"
echo "Sent Slack notification"
env:
LOGS_URL: https://github.com/coder/coder/actions/runs/${{ github.run_id }}