docs-v2/.github/workflows/doc-review.yml

349 lines
14 KiB
YAML

name: Doc Review
on:
pull_request:
types: [opened, synchronize, ready_for_review]
paths:
- 'content/**'
- 'layouts/**'
- 'assets/**'
- 'data/**'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: number
permissions: {}
concurrency:
group: doc-review-${{ github.event.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
# -----------------------------------------------------------------
# Job 1: Resolve preview URLs from changed content files
# -----------------------------------------------------------------
resolve-urls:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: |
github.event_name == 'workflow_dispatch' ||
(!github.event.pull_request.draft &&
github.event.pull_request.head.repo.full_name == github.repository &&
!contains(github.event.pull_request.labels.*.name, 'skip-review'))
outputs:
urls: ${{ steps.detect.outputs.urls }}
url-count: ${{ steps.detect.outputs.url-count }}
skipped: ${{ steps.detect.outputs.skipped }}
skip-reason: ${{ steps.detect.outputs.skip-reason }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 0
sparse-checkout: |
content
data/products.yml
scripts/lib/content-utils.js
.github/scripts/resolve-review-urls.js
package.json
sparse-checkout-cone-mode: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: Resolve base ref
id: base
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
if [ -n "${{ github.base_ref }}" ]; then
echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
else
BASE=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json baseRefName -q .baseRefName)
git fetch origin "$BASE"
echo "ref=origin/$BASE" >> "$GITHUB_OUTPUT"
fi
- name: Detect changed pages
id: detect
env:
BASE_REF: ${{ steps.base.outputs.ref }}
run: node .github/scripts/resolve-review-urls.js
# -----------------------------------------------------------------
# Job 2: Copilot code review (runs in parallel with Job 1)
# -----------------------------------------------------------------
copilot-review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: |
github.event_name == 'workflow_dispatch' ||
(!github.event.pull_request.draft &&
github.event.pull_request.head.repo.full_name == github.repository &&
!contains(github.event.pull_request.labels.*.name, 'skip-review'))
steps:
- name: Request Copilot review
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
with:
script: |
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
try {
await github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
reviewers: ['copilot-pull-request-reviewer'],
});
core.info('Copilot code review requested successfully');
} catch (error) {
core.warning(`Could not request Copilot review: ${error.message}`);
core.warning(
'To enable automatic Copilot reviews, configure a repository ruleset: ' +
'Settings → Rules → Rulesets → "Automatically request Copilot code review"'
);
}
# -----------------------------------------------------------------
# Job 3: Visual review check run (depends on Job 1 for URLs)
#
# Creates a GitHub Check Run (visible in the Checks tab) instead of
# posting a PR comment mentioning @github-copilot. A comment mention
# alone never invokes Copilot Vision; only a proper check run surfaces
# the visual-review status where reviewers can act on it.
# -----------------------------------------------------------------
copilot-visual-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
checks: write
needs: resolve-urls
if: needs.resolve-urls.result == 'success' && fromJson(needs.resolve-urls.outputs.url-count) > 0
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
sparse-checkout: .github/prompts/copilot-visual-review.md
sparse-checkout-cone-mode: false
- name: Get PR head SHA
id: pr-sha
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
with:
script: |
let sha = context.payload.pull_request?.head?.sha;
if (!sha) {
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
sha = pr.head.sha;
}
core.setOutput('sha', sha);
- name: Create in-progress check run
id: create-check
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
HEAD_SHA: ${{ steps.pr-sha.outputs.sha }}
with:
script: |
const { data: check } = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Visual Review',
head_sha: process.env.HEAD_SHA,
status: 'in_progress',
started_at: new Date().toISOString(),
output: {
title: 'Visual Review — Waiting for preview deployment',
summary: 'Waiting for PR preview to be available…',
},
});
core.setOutput('check-run-id', String(check.id));
core.info(`Created check run ${check.id}`);
- name: Wait for preview deployment
id: wait
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
PREVIEW_URL="https://influxdata.github.io/docs-v2/pr-preview/pr-${PR_NUMBER}/"
TIMEOUT=600 # 10 minutes
INTERVAL=15
ELAPSED=0
echo "Waiting for preview at ${PREVIEW_URL}"
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
STATUS=$(curl -s -o /dev/null -L -w "%{http_code}" "$PREVIEW_URL" || echo "000")
if [ "$STATUS" = "200" ]; then
echo "Preview is live"
echo "available=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Status: ${STATUS} (${ELAPSED}s / ${TIMEOUT}s)"
sleep "$INTERVAL"
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "Preview deployment timed out after ${TIMEOUT}s"
echo "available=false" >> "$GITHUB_OUTPUT"
- name: Complete check run — preview available
if: steps.wait.outputs.available == 'true'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PREVIEW_URLS: ${{ needs.resolve-urls.outputs.urls }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
CHECK_RUN_ID: ${{ steps.create-check.outputs.check-run-id }}
with:
script: |
const fs = require('fs');
const checkRunId = Number(process.env.CHECK_RUN_ID);
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const previewBase = `https://influxdata.github.io/docs-v2/pr-preview/pr-${prNumber}`;
let urls = [];
try { urls = JSON.parse(process.env.PREVIEW_URLS); } catch {}
const checklist = fs.readFileSync(
'.github/prompts/copilot-visual-review.md',
'utf8'
);
const pageList = urls.slice(0, 20)
.map(u => `- [${u}](${previewBase}${u})`)
.join('\n')
+ (urls.length > 20 ? `\n- … and ${urls.length - 20} more` : '');
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'completed',
conclusion: 'neutral',
completed_at: new Date().toISOString(),
output: {
title: `Visual Review — ${urls.length} page(s) ready for review`,
summary: `Preview is live with ${urls.length} changed page(s). Complete the checklist in the details to finish visual review.`,
text: `## Pages to Review\n\n${pageList}\n\n## Visual Review Checklist\n\n${checklist.trim()}`,
},
});
core.info(`Completed check run ${checkRunId} — ${urls.length} pages ready`);
- name: Complete check run — timed out
if: steps.wait.outputs.available == 'false'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
CHECK_RUN_ID: ${{ steps.create-check.outputs.check-run-id }}
with:
script: |
const checkRunId = Number(process.env.CHECK_RUN_ID);
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const previewUrl = `https://influxdata.github.io/docs-v2/pr-preview/pr-${prNumber}/`;
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'completed',
conclusion: 'neutral',
completed_at: new Date().toISOString(),
output: {
title: 'Visual Review — Preview deployment timed out',
summary: [
'Preview was not available within 10 minutes.',
'',
'To trigger visual review:',
`1. Wait for the [PR Preview](${previewUrl}) to deploy`,
'2. Re-run this workflow from the Actions tab',
].join('\n'),
},
});
core.info(`Completed check run ${checkRunId} — timed out`);
# -----------------------------------------------------------------
# Job 4: Report when visual review is skipped (no URLs to review)
#
# Creates a GitHub Check Run with conclusion 'skipped' so the Checks
# tab shows an accurate status rather than simply omitting the entry.
# -----------------------------------------------------------------
report-skipped:
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
needs: resolve-urls
# GitHub Actions outputs are always strings; 'true' string comparison is intentional
if: needs.resolve-urls.result == 'success' && needs.resolve-urls.outputs.skipped == 'true'
steps:
- name: Get PR head SHA
id: pr-sha
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
with:
script: |
let sha = context.payload.pull_request?.head?.sha;
if (!sha) {
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
sha = pr.head.sha;
}
core.setOutput('sha', sha);
- name: Create skipped visual review check run
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
HEAD_SHA: ${{ steps.pr-sha.outputs.sha }}
SKIP_REASON: ${{ needs.resolve-urls.outputs.skip-reason }}
with:
script: |
const skipReason = process.env.SKIP_REASON || 'No previewable content changes detected';
const now = new Date().toISOString();
const { data: check } = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Visual Review',
head_sha: process.env.HEAD_SHA,
status: 'completed',
conclusion: 'skipped',
started_at: now,
completed_at: now,
output: {
title: 'Visual Review — Skipped',
summary: skipReason,
text: [
'## What This Means',
'',
'- No preview pages were identified for visual review.',
'- This is expected for PRs that only change non-content files.',
'- Copilot code review (diff-based) still runs independently.',
'',
'## If You Expected Visual Review',
'',
'Check that your PR includes changes to files in `content/` that map to published documentation pages.',
].join('\n'),
},
});
core.info(`Created skipped check run ${check.id}: ${skipReason}`);