You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by mo...@apache.org on 2026/04/25 00:08:06 UTC
(doris-website) branch master updated: Improve docs AI review comment output (#3580)
This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new d33f93bfcf0 Improve docs AI review comment output (#3580)
d33f93bfcf0 is described below
commit d33f93bfcf0795522ebdc031de2e12429bdcac2e
Author: Mingyu Chen (Rayner) <mo...@163.com>
AuthorDate: Fri Apr 24 17:08:00 2026 -0700
Improve docs AI review comment output (#3580)
---
.github/workflows/docs-ai-review.yml | 73 +++----
package.json | 1 +
.../__tests__/docs-governance.test.js | 70 ++++++-
.../docs-governance/render-ai-review-comment.js | 219 +++++++++++++++++++++
4 files changed, 317 insertions(+), 46 deletions(-)
diff --git a/.github/workflows/docs-ai-review.yml b/.github/workflows/docs-ai-review.yml
index bdb14371647..23a1fe92b12 100644
--- a/.github/workflows/docs-ai-review.yml
+++ b/.github/workflows/docs-ai-review.yml
@@ -3,12 +3,6 @@ name: Docs AI Review
on:
issue_comment:
types: [created]
- workflow_dispatch:
- inputs:
- pr_number:
- description: Pull request number to run docs AI review for
- required: true
- type: string
# Same trust model as opencode-review.yml:
# - triggered by a trusted maintainer/collaborator comment
@@ -24,20 +18,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
if: >-
- github.event_name == 'workflow_dispatch' ||
+ github.event.issue.pull_request &&
+ contains(github.event.comment.body, '/review-docs') &&
(
- github.event.issue.pull_request &&
- contains(github.event.comment.body, '/review-docs') &&
- (
- github.event.comment.author_association == 'MEMBER' ||
- github.event.comment.author_association == 'OWNER' ||
- github.event.comment.author_association == 'COLLABORATOR'
- )
+ github.event.comment.author_association == 'MEMBER' ||
+ github.event.comment.author_association == 'OWNER' ||
+ github.event.comment.author_association == 'COLLABORATOR'
)
steps:
- name: Validate trigger command
- if: github.event_name == 'issue_comment'
run: |
body=$(jq -r '.comment.body' "$GITHUB_EVENT_PATH")
@@ -52,16 +42,10 @@ jobs:
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- EVENT_NAME: ${{ github.event_name }}
- DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail
- if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
- PR_NUMBER="$DISPATCH_PR_NUMBER"
- else
- PR_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
- fi
+ PR_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
@@ -124,6 +108,23 @@ jobs:
jq '{schema_version, changed_files, high_risk, review_agents: [.review_agents[].id], sync_groups: [.sync_groups[].sync_group_id]}' \
website-quality-governance/generated/ai-review-packet.json
+ - name: Run changed docs governance report
+ env:
+ BASE_SHA: ${{ steps.pr.outputs.base_sha }}
+ HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
+ run: |
+ set -euo pipefail
+
+ changed_files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" | paste -sd, -)
+
+ node scripts/docs-governance/report.js \
+ --files "$changed_files" \
+ --format json \
+ --output website-quality-governance/generated/docs-governance-report.json
+
+ jq '{summary, findings: [.findings[] | {severity, rule, path, line, message, related_paths}]}' \
+ website-quality-governance/generated/docs-governance-report.json
+
- name: Upsert advisory PR comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -134,29 +135,15 @@ jobs:
set -euo pipefail
packet=website-quality-governance/generated/ai-review-packet.json
+ report=website-quality-governance/generated/docs-governance-report.json
marker='<!-- docs-ai-review:packet -->'
- changed_count=$(jq '.changed_files | length' "$packet")
- high_risk=$(jq -r '.high_risk.required // false' "$packet")
- agent_count=$(jq '.review_agents | length' "$packet")
- sync_group_count=$(jq '.sync_groups | length' "$packet")
-
- cat > /tmp/docs-ai-review-comment.md <<EOF_COMMENT
- $marker
- ## Docs AI Review
-
- Docs AI review packet prepared for this PR.
-
- - Trigger: \`/review-docs\`
- - Changed files in packet: \`$changed_count\`
- - High-risk docs path matched: \`$high_risk\`
- - Review agents included: \`$agent_count\`
- - Sync groups included: \`$sync_group_count\`
- - Base SHA: \`$BASE_SHA\`
- - Head SHA: \`$HEAD_SHA\`
-
- This MVP prepares the packet for advisory AI review. AI findings, when produced, are suggestions only; deterministic governance CI remains the blocking signal.
- EOF_COMMENT
+ node scripts/docs-governance/render-ai-review-comment.js \
+ --packet "$packet" \
+ --report "$report" \
+ --base-sha "$BASE_SHA" \
+ --head-sha "$HEAD_SHA" \
+ --output /tmp/docs-ai-review-comment.md
existing_comment_id=$(
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
diff --git a/package.json b/package.json
index a7a7a568784..e133faa642c 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"docs:ai-review:prepare": "node scripts/docs-governance/prepare-ai-review.js",
"docs:ai-review:validate": "node scripts/docs-governance/validate-ai-review-output.js",
"docs:ai-review:dedupe": "node scripts/docs-governance/dedupe-ai-review-comments.js",
+ "docs:ai-review:comment": "node scripts/docs-governance/render-ai-review-comment.js",
"docs:lint": "node scripts/docs-governance/report.js --format github",
"docs:lint:changed": "node scripts/docs-governance/report.js --changed --format github",
"docs:report": "node scripts/docs-governance/report.js --format json --output website-quality-governance/generated/docs-governance-report.json",
diff --git a/scripts/docs-governance/__tests__/docs-governance.test.js b/scripts/docs-governance/__tests__/docs-governance.test.js
index 7d143f8f13a..575dc782df4 100644
--- a/scripts/docs-governance/__tests__/docs-governance.test.js
+++ b/scripts/docs-governance/__tests__/docs-governance.test.js
@@ -1423,6 +1423,64 @@ test('dedupeAiReviewFindings returns stable unique findings', () => {
);
});
+test('renderDocsAiReviewComment lists actionable changed-file findings', () => {
+ const { renderDocsAiReviewComment } = require('../render-ai-review-comment');
+ const comment = renderDocsAiReviewComment({
+ packet: {
+ changed_files: ['docs/sql-manual/sql-functions/bad-function.md'],
+ high_risk: {
+ required: true,
+ matched_paths: [
+ {
+ path: 'docs/sql-manual/sql-functions/bad-function.md',
+ rule: 'docs/sql-manual/',
+ },
+ ],
+ },
+ review_agents: [{ id: 'docs-clarity' }, { id: 'i18n-version-sync' }],
+ sync_groups: [{ sync_group_id: 'main_docs:sql-manual/sql-functions/bad-function' }],
+ },
+ report: {
+ summary: {
+ findings: 2,
+ by_severity: { error: 1, warning: 1 },
+ },
+ findings: [
+ {
+ severity: 'error',
+ rule: 'sql-function-section-order',
+ path: 'docs/sql-manual/sql-functions/bad-function.md',
+ line: 12,
+ message: 'SQL function docs must contain sections in the required order.',
+ owner: '@apache/doris-website-maintainers',
+ },
+ {
+ severity: 'warning',
+ rule: 'i18n-sync-locale-counterpart',
+ path: 'docs/sql-manual/sql-functions/bad-function.md',
+ line: 1,
+ message: 'Chinese localized counterpart needs the same update.',
+ related_paths: [
+ 'i18n/zh-CN/docusaurus-plugin-content-docs/current/sql-manual/sql-functions/bad-function.md',
+ ],
+ },
+ ],
+ },
+ baseSha: '9f5aa557f929f98e064426fcabe90dfc40dc743e',
+ headSha: '2288afd2f3727fa50c438ee3d2be1a813ee7d292',
+ });
+
+ assert.match(comment, /<!-- docs-ai-review:packet -->/);
+ assert.match(comment, /Findings to fix/);
+ assert.match(comment, /`docs\/sql-manual\/sql-functions\/bad-function.md:12`/);
+ assert.match(comment, /sql-function-section-order/);
+ assert.match(comment, /Update the SQL function page so it follows the required function documentation structure/);
+ assert.match(comment, /i18n\/zh-CN\/docusaurus-plugin-content-docs\/current\/sql-manual\/sql-functions\/bad-function.md/);
+ assert.match(comment, /High-risk docs path matched: `true`/);
+ assert.match(comment, /Base SHA: `9f5aa557/);
+ assert.match(comment, /Head SHA: `2288afd2/);
+});
+
test('Week 7 AI review scripts and workflow are exposed without replacing the generic review workflow', () => {
const rootDir = path.join(__dirname, '..', '..', '..');
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
@@ -1438,12 +1496,18 @@ test('Week 7 AI review scripts and workflow are exposed without replacing the ge
assert.equal(packageJson.scripts['docs:ai-review:prepare'], 'node scripts/docs-governance/prepare-ai-review.js');
assert.equal(packageJson.scripts['docs:ai-review:validate'], 'node scripts/docs-governance/validate-ai-review-output.js');
assert.equal(packageJson.scripts['docs:ai-review:dedupe'], 'node scripts/docs-governance/dedupe-ai-review-comments.js');
+ assert.equal(packageJson.scripts['docs:ai-review:comment'], 'node scripts/docs-governance/render-ai-review-comment.js');
assert.match(docsAiReviewWorkflow, /\/review-docs/);
- assert.match(docsAiReviewWorkflow, /ai-review-docs/);
- assert.match(docsAiReviewWorkflow, /pull-requests: read/);
+ assert.match(docsAiReviewWorkflow, /issue_comment:/);
+ assert.doesNotMatch(docsAiReviewWorkflow, /workflow_dispatch:/);
+ assert.match(docsAiReviewWorkflow, /docs-governance-report\.json/);
+ assert.match(docsAiReviewWorkflow, /render-ai-review-comment\.js/);
+ assert.doesNotMatch(docsAiReviewWorkflow, /DISPATCH_PR_NUMBER/);
+ assert.match(docsAiReviewWorkflow, /pull-requests: write/);
assert.match(docsAiReviewWorkflow, /contents: read/);
assert.match(docsAiReviewWorkflow, /issues: write/);
- assert.ok(docsAiReviewWorkflow.indexOf('Checkout PR head') < docsAiReviewWorkflow.indexOf('issues: write'));
+ assert.ok(docsAiReviewWorkflow.indexOf('Checkout PR head') < docsAiReviewWorkflow.indexOf('Run changed docs governance report'));
+ assert.ok(docsAiReviewWorkflow.indexOf('Run changed docs governance report') < docsAiReviewWorkflow.indexOf('Upsert advisory PR comment'));
assert.match(openCodeWorkflow, /\/review/);
});
diff --git a/scripts/docs-governance/render-ai-review-comment.js b/scripts/docs-governance/render-ai-review-comment.js
new file mode 100644
index 00000000000..2c2a0edf8b1
--- /dev/null
+++ b/scripts/docs-governance/render-ai-review-comment.js
@@ -0,0 +1,219 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { ensureDirForFile, parseArgs } = require('./lib');
+
+const MARKER = '<!-- docs-ai-review:packet -->';
+const DEFAULT_MAX_FINDINGS = 25;
+const SEVERITY_ORDER = {
+ error: 0,
+ warning: 1,
+ info: 2,
+};
+
+function readJson(filePath) {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+}
+
+function shortSha(sha) {
+ return sha ? String(sha).slice(0, 12) : 'unknown';
+}
+
+function inlineCode(value) {
+ const normalized = value === undefined || value === null || value === '' ? 'unknown' : value;
+ return `\`${String(normalized).replace(/`/g, '\\`')}\``;
+}
+
+function countBySeverity(findings, summary = {}) {
+ const fromSummary = summary.by_severity || {};
+ return {
+ error: fromSummary.error || findings.filter((finding) => finding.severity === 'error').length,
+ warning: fromSummary.warning || findings.filter((finding) => finding.severity === 'warning').length,
+ info: fromSummary.info || findings.filter((finding) => finding.severity === 'info').length,
+ };
+}
+
+function sortFindings(findings) {
+ return [...findings].sort((left, right) => {
+ const severity = (SEVERITY_ORDER[left.severity] ?? 99) - (SEVERITY_ORDER[right.severity] ?? 99);
+ if (severity !== 0) {
+ return severity;
+ }
+ const pathCompare = String(left.path || '').localeCompare(String(right.path || ''));
+ if (pathCompare !== 0) {
+ return pathCompare;
+ }
+ const lineCompare = (left.line || 0) - (right.line || 0);
+ if (lineCompare !== 0) {
+ return lineCompare;
+ }
+ return String(left.rule || '').localeCompare(String(right.rule || ''));
+ });
+}
+
+function suggestActionForFinding(finding) {
+ const rule = String(finding.rule || '');
+
+ if (rule.startsWith('sql-function-')) {
+ return 'Update the SQL function page so it follows the required function documentation structure, examples, return type, and NULL-handling coverage.';
+ }
+ if (rule.startsWith('feature-doc-')) {
+ return 'Complete the feature page with overview, basic usage, options, realistic examples, caveats, and best practices where applicable.';
+ }
+ if (rule.startsWith('i18n-sync-')) {
+ return 'Update the current, versioned, and localized counterpart pages consistently, or document why this change is intentionally asymmetric.';
+ }
+ if (rule.startsWith('link-')) {
+ return 'Fix the target path, heading anchor, inbound reference, or redirect so readers do not hit broken navigation.';
+ }
+ if (rule.startsWith('sidebar-')) {
+ return 'Update the sidebar/navigation entry so it matches the actual document path and intended information architecture.';
+ }
+ if (rule.startsWith('frontmatter-')) {
+ return 'Correct the Docusaurus front matter metadata so the page renders, routes, and appears in search correctly.';
+ }
+ if (rule.startsWith('markdown-')) {
+ return 'Fix the Markdown structure so headings, code fences, and document hierarchy are unambiguous.';
+ }
+ if (rule.startsWith('seo-')) {
+ return 'Improve the SEO/GEO metadata or search policy so users and AI answer engines get clear page context.';
+ }
+
+ return 'Review the finding and update the affected documentation so the reader experience remains complete and unambiguous.';
+}
+
+function formatFinding(finding, index) {
+ const severity = String(finding.severity || 'warning').toUpperCase();
+ const location = finding.line ? `${finding.path}:${finding.line}` : finding.path;
+ const lines = [
+ `${index}. **${severity}** ${inlineCode(location)} - ${inlineCode(finding.rule)}`,
+ ` - Problem: ${finding.message || 'No finding message provided.'}`,
+ ` - Fix: ${suggestActionForFinding(finding)}`,
+ ];
+
+ if (finding.related_paths && finding.related_paths.length) {
+ lines.push(` - Related: ${finding.related_paths.map(inlineCode).join(', ')}`);
+ }
+ if (finding.owner) {
+ lines.push(` - Owner: ${finding.owner}`);
+ }
+
+ return lines.join('\n');
+}
+
+function formatLimitedList(values, limit = 10) {
+ const uniqueValues = [...new Set(values.filter(Boolean))];
+ const shown = uniqueValues.slice(0, limit);
+ const lines = shown.map((value) => `- ${inlineCode(value)}`);
+ if (uniqueValues.length > shown.length) {
+ lines.push(`- ... ${uniqueValues.length - shown.length} more`);
+ }
+ return lines;
+}
+
+function renderDocsAiReviewComment(options = {}) {
+ const packet = options.packet || {};
+ const report = options.report || {};
+ const findings = sortFindings(Array.isArray(report.findings) ? report.findings : []);
+ const maxFindings = Number.isFinite(options.maxFindings) ? options.maxFindings : DEFAULT_MAX_FINDINGS;
+ const shownFindings = findings.slice(0, maxFindings);
+ const severityCounts = countBySeverity(findings, report.summary);
+ const changedFiles = Array.isArray(packet.changed_files) ? packet.changed_files : [];
+ const highRisk = packet.high_risk || {};
+ const highRiskMatches = Array.isArray(highRisk.matched_paths) ? highRisk.matched_paths : [];
+ const reviewAgents = Array.isArray(packet.review_agents) ? packet.review_agents : [];
+ const syncGroups = Array.isArray(packet.sync_groups) ? packet.sync_groups : [];
+
+ const lines = [
+ MARKER,
+ '## Docs AI Review',
+ '',
+ 'Changed-file docs review results for this PR.',
+ '',
+ '### Summary',
+ `- Trigger: ${inlineCode('/review-docs')}`,
+ `- Changed files reviewed: ${inlineCode(changedFiles.length)}`,
+ `- Findings: ${inlineCode(findings.length)} (${severityCounts.error} error, ${severityCounts.warning} warning, ${severityCounts.info} info)`,
+ `- High-risk docs path matched: ${inlineCode(Boolean(highRisk.required))}`,
+ `- Review agents in packet: ${inlineCode(reviewAgents.length)}`,
+ `- Sync groups in packet: ${inlineCode(syncGroups.length)}`,
+ `- Base SHA: ${inlineCode(shortSha(options.baseSha))}`,
+ `- Head SHA: ${inlineCode(shortSha(options.headSha))}`,
+ '',
+ '### Findings to fix',
+ ];
+
+ if (shownFindings.length) {
+ lines.push(...shownFindings.map((finding, index) => formatFinding(finding, index + 1)));
+ if (findings.length > shownFindings.length) {
+ lines.push('', `_${findings.length - shownFindings.length} additional findings are not shown. Check the docs governance job output for the full list._`);
+ }
+ } else {
+ lines.push('No changed-file governance findings were produced. Review the high-risk scope and sync groups below before merging.');
+ }
+
+ if (highRiskMatches.length) {
+ lines.push(
+ '',
+ '### High-risk scope',
+ ...formatLimitedList(highRiskMatches.map((match) => `${match.path} (${match.rule})`)),
+ );
+ }
+
+ if (syncGroups.length) {
+ lines.push(
+ '',
+ '### Sync groups to verify',
+ ...formatLimitedList(syncGroups.map((group) => group.sync_group_id)),
+ );
+ }
+
+ lines.push(
+ '',
+ '### Next steps',
+ '- Fix the findings above, especially errors and warnings.',
+ '- Confirm current, active versioned, and localized docs stay aligned when the same topic is changed.',
+ '- Re-run `/review-docs` after updating the PR.',
+ '',
+ 'AI review output is advisory. Deterministic governance CI remains the blocking signal.',
+ );
+
+ return `${lines.join('\n')}\n`;
+}
+
+function runCli() {
+ const args = parseArgs(process.argv.slice(2));
+ if (!args.packet || !args.report) {
+ throw new Error('Usage: node scripts/docs-governance/render-ai-review-comment.js --packet <packet.json> --report <report.json> [--output <comment.md>]');
+ }
+
+ const packet = readJson(path.resolve(process.cwd(), args.packet));
+ const report = readJson(path.resolve(process.cwd(), args.report));
+ const maxFindings = args['max-findings'] ? Number(args['max-findings']) : DEFAULT_MAX_FINDINGS;
+ const comment = renderDocsAiReviewComment({
+ packet,
+ report,
+ baseSha: args['base-sha'],
+ headSha: args['head-sha'],
+ maxFindings,
+ });
+
+ if (args.output) {
+ const outputPath = path.resolve(process.cwd(), args.output);
+ ensureDirForFile(outputPath);
+ fs.writeFileSync(outputPath, comment, 'utf8');
+ } else {
+ process.stdout.write(comment);
+ }
+}
+
+if (require.main === module) {
+ runCli();
+}
+
+module.exports = {
+ MARKER,
+ renderDocsAiReviewComment,
+ suggestActionForFinding,
+};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org