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

302 lines
11 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 }}
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: Copilot visual review (depends on Job 1 for URLs)
# -----------------------------------------------------------------
copilot-visual-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
statuses: read
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: Wait for preview deployment
id: wait
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const TIMEOUT_MS = 180_000; // 3 minutes
const INTERVAL_MS = 15_000; // 15 seconds
const CONTEXT = 'preview/deploy';
const sha = context.sha;
const start = Date.now();
while (Date.now() - start < TIMEOUT_MS) {
const { data: statuses } = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: sha,
});
const status = statuses.find(s => s.context === CONTEXT);
if (status) {
if (status.state === 'success') {
core.info(`Preview deployed: ${status.target_url}`);
core.setOutput('available', 'true');
core.setOutput('preview-url', status.target_url);
return;
}
if (status.state === 'failure' || status.state === 'error') {
core.info(`Preview failed: ${status.description}`);
core.setOutput('available', 'false');
core.setOutput('reason', status.description);
return;
}
core.info(`Preview building... (${Math.round((Date.now() - start) / 1000)}s)`);
} else {
core.info(`No preview status yet (${Math.round((Date.now() - start) / 1000)}s)`);
}
await new Promise(r => setTimeout(r, INTERVAL_MS));
}
core.info('Preview status check timed out');
core.setOutput('available', 'false');
core.setOutput('reason', 'Timed out waiting for preview status');
- name: Post visual review request
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 }}
with:
script: |
const fs = require('fs');
let urls;
try {
urls = JSON.parse(process.env.PREVIEW_URLS);
} catch (e) {
core.warning(`Failed to parse PREVIEW_URLS: ${e.message}`);
return;
}
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const previewBase = `https://influxdata.github.io/docs-v2/pr-preview/pr-${prNumber}`;
// Build preview URL list
const urlList = urls
.map(u => `- [${u}](${previewBase}${u})`)
.join('\n');
// Read the Copilot visual review template
const template = fs.readFileSync(
'.github/prompts/copilot-visual-review.md',
'utf8'
);
const marker = '<!-- doc-review-visual -->';
const body = [
marker,
'## Preview Pages for Review',
'',
`${urls.length} page(s) changed in this PR:`,
'',
'<details>',
'<summary>Preview URLs</summary>',
'',
urlList,
'',
'</details>',
'',
'---',
'',
`@github-copilot please review the preview pages listed above using the template below:`,
'',
template.trim(),
'',
urlList,
].join('\n');
// Update existing comment or create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
core.info(`Posted visual review request with ${urls.length} URLs`);
- name: Post timeout notice
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 }}
with:
script: |
const prNumber = context.issue.number || Number(process.env.PR_NUMBER);
const reason = '${{ steps.wait.outputs.reason }}' || 'Unknown';
const marker = '<!-- doc-review-visual-timeout -->';
const body = [
marker,
'## Visual Review Skipped',
'',
`Reason: ${reason}`,
'',
'Visual review was skipped. The Copilot code review (Job 2) still ran.',
'',
'To trigger visual review manually, re-run this workflow after the',
'preview is deployed.',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}