diff --git a/.github/workflows/docs-preview.yaml b/.github/workflows/docs-preview.yaml index 12e619c27c..c585a61acd 100644 --- a/.github/workflows/docs-preview.yaml +++ b/.github/workflows/docs-preview.yaml @@ -2,8 +2,13 @@ # pull request that touches files under docs/ is opened. The preview # is served by coder.com's branch-preview feature at /docs/@. # +# The link deep-links to the first added/modified/renamed Markdown file +# under docs/ so reviewers land on the page that actually changed. # Branch names are URL-encoded so that names containing slashes or # other special characters produce working links. +# +# If the PR only deletes Markdown files (or only changes non-Markdown +# files such as images or manifest.json), no comment is posted. name: docs-preview @@ -30,14 +35,71 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} run: | + # Fetch the list of non-deleted files from the PR. This is + # intentionally not piped into grep so that a gh-api failure + # (network, auth, rate-limit) propagates immediately instead + # of being swallowed by `|| true`. + all_files=$(gh api --paginate \ + "repos/${REPO}/pulls/${PR_NUMBER}/files" \ + --jq '.[] | select(.status != "removed") | .filename') + + # Pick the first Markdown file under docs/. `|| true` keeps + # the pipeline from failing when grep finds no matches or + # head triggers SIGPIPE under `set -o pipefail`. + first_doc=$(printf '%s\n' "$all_files" \ + | grep -E '^docs/.*\.md$' \ + | head -n 1) || true + + if [ -z "$first_doc" ]; then + echo "No added/modified Markdown files under docs/, skipping preview comment." + exit 0 + fi + + # Map the repo path to the docs site URL path. + # docs/README.md -> "" (docs root) + # docs//index.md -> "" (directory index) + # docs//README.md -> "" (directory index) + # docs//.md -> "/" + rel="${first_doc#docs/}" + case "$rel" in + README.md) + page_path="" + ;; + *) + base="$(basename "$rel")" + dir="$(dirname "$rel")" + if [ "$dir" = "." ]; then + dir="" + fi + case "$base" in + index.md|README.md) + page_path="$dir" + ;; + *) + stripped="${base%.md}" + if [ -z "$dir" ]; then + page_path="$stripped" + else + page_path="${dir}/${stripped}" + fi + ;; + esac + ;; + esac + # URL-encode the branch name so slashes and special - # characters don't break the preview URL. - encoded=$(jq -rn --arg b "$BRANCH" '$b | @uri') - url="https://coder.com/docs/@${encoded}" + # characters don't break the preview URL. The page path is + # left as-is because its components are simple ASCII path + # segments and the slashes between them must be preserved. + encoded_branch=$(jq -rn --arg b "$BRANCH" '$b | @uri') + url="https://coder.com/docs/@${encoded_branch}" + if [ -n "$page_path" ]; then + url="${url}/${page_path}" + fi gh pr comment "${PR_NUMBER}" \ --repo "${REPO}" \ --body "## Docs preview - [:book: View docs preview](${url}) + [:book: View docs preview](${url}) for \`${first_doc}\` " diff --git a/.github/workflows/test-docs-preview-mapper.sh b/.github/workflows/test-docs-preview-mapper.sh new file mode 100755 index 0000000000..6ebf9e7473 --- /dev/null +++ b/.github/workflows/test-docs-preview-mapper.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Regression tests for the path-mapping logic in docs-preview.yaml. +# The mapper converts a repo-relative docs path into the URL path +# used by the docs site preview. Five distinct branches exist in the +# case block; every branch must be covered here. + +set -euo pipefail + +# map_doc_path replicates the case block from docs-preview.yaml so +# we can exercise it without running the full workflow. +map_doc_path() { + local first_doc="$1" + local rel="${first_doc#docs/}" + local page_path + + case "$rel" in + README.md) + page_path="" + ;; + *) + local base dir stripped + base="$(basename "$rel")" + dir="$(dirname "$rel")" + if [ "$dir" = "." ]; then + dir="" + fi + case "$base" in + index.md | README.md) + page_path="$dir" + ;; + *) + stripped="${base%.md}" + if [ -z "$dir" ]; then + page_path="$stripped" + else + page_path="${dir}/${stripped}" + fi + ;; + esac + ;; + esac + + printf '%s' "$page_path" +} + +failures=0 + +assert_maps_to() { + local input="$1" + local expected="$2" + local actual + actual="$(map_doc_path "$input")" + if [ "$actual" = "$expected" ]; then + echo "PASS: $input -> \"$expected\"" + else + echo "FAIL: $input -> \"$actual\" (expected \"$expected\")" + failures=$((failures + 1)) + fi +} + +# Branch 1: top-level README maps to the docs root. +assert_maps_to "docs/README.md" "" + +# Branch 2: nested index.md strips the filename, leaving the dir. +assert_maps_to "docs/install/index.md" "install" + +# Branch 3: nested README.md behaves the same as index.md. +assert_maps_to "docs/admin/README.md" "admin" + +# Branch 4: nested regular file strips .md and keeps the dir prefix. +assert_maps_to "docs/ai-coder/tasks.md" "ai-coder/tasks" + +# Branch 5: top-level non-README file strips .md with no dir prefix. +assert_maps_to "docs/CHANGELOG.md" "CHANGELOG" + +# Additional coverage for edge cases and deeper nesting. +assert_maps_to "docs/index.md" "" +assert_maps_to "docs/about/contributing/CONTRIBUTING.md" "about/contributing/CONTRIBUTING" +assert_maps_to "docs/admin/groups.md" "admin/groups" +assert_maps_to "docs/tutorials/best-practices/index.md" "tutorials/best-practices" + +if [ "$failures" -gt 0 ]; then + echo "" + echo "$failures test(s) failed." + exit 1 +fi + +echo "" +echo "All tests passed."