Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
330 changes: 330 additions & 0 deletions .github/workflows/doc-style-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
name: Doc Style Checker (Vale Compatible)

on:
workflow_call:
inputs:
repository:
description: "The repository to check out"
required: true
type: string
path:
type: string
description: "The startPath pointing to the folder containing documentation"
required: false
default: "."
pull_request_number:
type: string
description: "The pull request number to check out"
required: true
base_sha:
type: string
description: "The base sha to diff against"
required: true
head_sha:
type: string
description: "The head sha to comment against"
required: true
secrets:
GEMINI_API_KEY:
description: "Google Gemini API key"
required: true
GEMINI_API_KEY_2:
description: "Google Gemini API key"
required: true
GEMINI_API_KEY_3:
description: "Google Gemini API key"
required: true
ACTION_TOKEN:
description: "GitHub token for posting comments"
required: true

jobs:
doc-style-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
repository: ${{ inputs.repository }}
ref: ${{ inputs.head_sha }}
fetch-depth: 100
path: content-repo

- name: Get changed documentation files
id: changed-files
working-directory: ./content-repo
run: |
echo "Getting changed files between ${{ inputs.base_sha }} and ${{ inputs.head_sha }}"
CHANGED_FILES=$(git diff --name-only ${{ inputs.base_sha }} ${{ inputs.head_sha }} | grep -E '\.adoc$' || true)
echo "Changed documentation files:"
echo "$CHANGED_FILES"
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Create git diff for parsing
if: steps.changed-files.outputs.files != ''
working-directory: ./content-repo
run: |
git diff ${{ inputs.base_sha }} ${{ inputs.head_sha }} > ../diff.txt
echo "Diff created with $(wc -l < ../diff.txt) lines"

- name: Setup Node.js
if: steps.changed-files.outputs.files != ''
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Install dependencies
if: steps.changed-files.outputs.files != ''
run: |
npm init -y
npm install handlebars

- name: Process files and get AI suggestions
if: steps.changed-files.outputs.files != ''
working-directory: ./content-repo
run: |
cat > ../process_docs.js << 'EOF'
const fs = require('fs');
const https = require('https');

// Simple git diff parser (avoiding ES module issues)
function parseDiffFile(filePath) {
const diffContent = fs.readFileSync(filePath, "utf-8");
const lines = diffContent.split('\n');
const fileChanges = {};

let currentFile = null;
let currentHunk = null;
let lineNumber = 0;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];

// Look for file headers
if (line.startsWith('diff --git')) {
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
if (match && match[1].endsWith('.adoc')) {
currentFile = match[1];
fileChanges[currentFile] = {};
}
continue;
}

// Look for hunk headers
if (line.startsWith('@@') && currentFile) {
const match = line.match(/@@ -\d+,?\d* \+(\d+),?(\d*) @@/);
if (match) {
lineNumber = parseInt(match[1], 10);
currentHunk = true;
}
continue;
}

// Process added lines in the current file
if (currentFile && currentHunk && line.startsWith('+') && !line.startsWith('+++')) {
const content = line.substring(1);
fileChanges[currentFile][lineNumber] = content;
lineNumber++;
} else if (currentFile && currentHunk && !line.startsWith('-') && !line.startsWith('\\')) {
// Context lines and unchanged lines
if (line.startsWith(' ') || (!line.startsWith('+') && !line.startsWith('-'))) {
lineNumber++;
}
}

// Reset when we hit a new file
if (line.startsWith('diff --git') || line.startsWith('index ')) {
currentHunk = false;
}
}

return ((file, line) => {
return fileChanges[file] && fileChanges[file][line] ? fileChanges[file][line] : null;
});
}

// Call your new Vale-compatible endpoint
async function checkDocStyleVale(content, filename) {
const data = JSON.stringify({ content, filename });

const options = {
hostname: 'doc-style-checker.vercel.app',
path: '/api/check-style-vale-format',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data, 'utf8'),
'User-Agent': 'GitHub-Actions-Doc-Checker-Vale/1.0'
}
};

return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => { responseData += chunk; });
res.on('end', () => {
try {
if (res.statusCode !== 200) {
throw new Error(`HTTP ${res.statusCode}: ${responseData}`);
}
resolve(JSON.parse(responseData));
} catch (e) {
reject(e);
}
});
});

req.on('error', reject);
req.setTimeout(45000, () => {
req.destroy();
reject(new Error('Request timeout'));
});

req.write(data);
req.end();
});
}

async function processAllFiles() {
const changedFiles = process.env.CHANGED_FILES.split('\n').filter(f => f.trim());
const inDiff = parseDiffFile('../diff.txt');
const results = {};

for (const file of changedFiles) {
if (!file.trim()) continue;

try {
console.log(`Processing ${file}...`);
const content = fs.readFileSync(file, 'utf8');
const valeResult = await checkDocStyleVale(content, file);

// Filter results to only include lines that changed
const fileRules = valeResult[file] || [];
const filteredRules = fileRules.filter(rule => {
const changedContent = inDiff(file, rule.Line);
return changedContent !== null;
});

if (filteredRules.length > 0) {
// Group by line number manually (avoiding Object.groupBy compatibility issues)
const rulesByLine = {};
filteredRules.forEach(rule => {
const line = rule.Line;
if (!rulesByLine[line]) {
rulesByLine[line] = [];
}
rulesByLine[line].push(rule);
});

for (const lineNum of Object.keys(rulesByLine)) {
const lineContent = inDiff(file, parseInt(lineNum));
rulesByLine[lineNum] = {
rules: rulesByLine[lineNum],
pre: lineContent,
new: lineContent // Will be modified by AI suggestion
};
}
results[file] = rulesByLine;
}

// Rate limiting
await new Promise(resolve => setTimeout(resolve, 2000));

} catch (error) {
console.error(`Error processing ${file}:`, error.message);
results[file] = { error: error.message };
}
}

return results;
}

processAllFiles()
.then(results => {
fs.writeFileSync('/tmp/vale-results.json', JSON.stringify(results, null, 2));
console.log('✓ Vale-compatible results generated');
})
.catch(error => {
console.error('❌ Processing failed:', error);
process.exit(1);
});
EOF

CHANGED_FILES="${{ steps.changed-files.outputs.files }}" node ../process_docs.js

- name: Post PR Comments
if: steps.changed-files.outputs.files != ''
uses: actions/github-script@v7
with:
github-token: ${{ secrets.ACTION_TOKEN }}
script: |
const fs = require('fs');

let results = {};
try {
const resultsData = fs.readFileSync('/tmp/vale-results.json', 'utf8');
results = JSON.parse(resultsData);
} catch (error) {
console.log('No results to process');
return;
}

// Delete existing comments from the bot
const comments = await github.rest.pulls.listReviewComments({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ inputs.pull_request_number }},
per_page: 100
});

const botComments = comments.data.filter(
comment => comment.body.includes('Automated review comment from Gemini AI')
);

await Promise.all(botComments.map(comment =>
github.rest.pulls.deleteReviewComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id
})
));

// Post new comments for each issue
for (const [file, lines] of Object.entries(results)) {
if (lines.error) {
console.error(`Error in ${file}: ${lines.error}`);
continue;
}

for (const [lineNum, record] of Object.entries(lines)) {
if (record.rules && record.rules.length > 0) {
const rulesList = record.rules.map(rule =>
`* **${rule.Check}** (${rule.Severity}) - ${rule.Message}`
).join('\n');

const commentBody = "**Automated review comment from Gemini AI:**\n\n" +
"```suggestion\n" + record.pre + "\n```\n\n" +
"**Style Guide Issues Found:**\n" + rulesList + "\n\n" +
"---\n*Powered by [Doc Style Checker](https://doc-style-checker.vercel.app/) using Couchbase Documentation Style Guide*";

try {
await github.rest.pulls.createReviewComment({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ inputs.pull_request_number }},
body: commentBody,
commit_id: '${{ inputs.head_sha }}',
path: file,
line: parseInt(lineNum),
side: 'RIGHT'
});

console.log(`✓ Posted comment for ${file}:${lineNum}`);
} catch (error) {
console.error(`❌ Failed to post comment for ${file}:${lineNum}:`, error.message);
}
}
}
}
23 changes: 23 additions & 0 deletions .github/workflows/docs-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Documentation Review (Vale Compatible)

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**/*.adoc'
- '**/*.md'

jobs:
doc-style-check:
uses: couchbaselabs/docs-runner/.github/workflows/doc-style-checker.yml@fi-docs-style-checker
with:
repository: ${{ github.repository }}
path: "."
pull_request_number: ${{ github.event.pull_request.number }}
base_sha: ${{ github.event.pull_request.base.sha }}
head_sha: ${{ github.event.pull_request.head.sha }}
secrets:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_API_KEY_2: ${{ secrets.GEMINI_API_KEY_2 }}
GEMINI_API_KEY_3: ${{ secrets.GEMINI_API_KEY_3 }}
ACTION_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 changes: 0 additions & 16 deletions .github/workflows/fi-test.yml

This file was deleted.

Loading