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
362 changes: 362 additions & 0 deletions .github/workflows/doc-style-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
name: Doc Style Checker

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: "Optional second Google Gemini API key"
required: false
GEMINI_API_KEY_3:
description: "Optional third Google Gemini API key"
required: false
ACTION_TOKEN:
description: "GitHub token for posting comments"
required: true
workflow_dispatch: # Add this for manual testing
inputs:
test_mode:
description: "Run in test mode"
default: true
type: boolean

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|md)$' || true)
echo "Changed documentation files:"
echo "$CHANGED_FILES"
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Process changed documentation files
if: steps.changed-files.outputs.files != ''
working-directory: ./content-repo
run: |
mkdir -p /tmp/doc-processing
echo "Processing files..."

echo '${{ steps.changed-files.outputs.files }}' | while IFS= read -r file; do
if [[ -n "$file" && -f "$file" ]]; then
echo "Preparing: $file"
SAFE_NAME=$(echo "$(basename "$file")" | sed 's/[^a-zA-Z0-9._-]/_/g')

# Create a simple JSON file with proper escaping
python3 -c "
import json
import sys

filename = '$file'

try:
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()

data = {
'filename': filename,
'content': content
}

with open('/tmp/doc-processing/${SAFE_NAME}.meta', 'w') as f:
json.dump(data, f)

print(f'✓ Prepared {filename}')
except Exception as e:
print(f'✗ Error processing {filename}: {e}')
sys.exit(1)
"
fi
done

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

- name: Run Doc Style Checker
if: steps.changed-files.outputs.files != ''
run: |
cat > check_docs.js << 'EOF'
const fs = require('fs');
const path = require('path');
const https = require('https');

async function checkDocStyle(content, filename) {
console.log(`📤 Sending request for: ${filename}`);
console.log(`📝 Content preview: ${content.substring(0, 100)}...`);
console.log(`📏 Content length: ${content.length}`);

const data = JSON.stringify({
content: content,
filename: filename
});

console.log(`📦 Request data size: ${data.length} bytes`);

const options = {
hostname: 'doc-style-checker.vercel.app',
path: '/api/check-style',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length,
'User-Agent': 'GitHub-Actions-Doc-Checker/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 {
console.log(`📨 Response status: ${res.statusCode}`);
console.log(`📨 Response headers:`, JSON.stringify(res.headers));
console.log(`📨 Response body preview: ${responseData.substring(0, 500)}...`);

if (res.statusCode !== 200) {
console.error(`❌ Non-200 status code: ${res.statusCode}`);
console.error(`❌ Full response: ${responseData}`);
throw new Error(`HTTP ${res.statusCode}: ${responseData}`);
}
const result = JSON.parse(responseData);
console.log(`✅ Successfully parsed response for ${filename}`);
resolve(result);
} catch (e) {
console.error('❌ Response parsing error:', e.message);
console.error('❌ Raw response:', responseData);
reject(e);
}
});
});

req.on('error', (e) => {
console.error(`❌ Request error for ${filename}:`, e.message);
reject(e);
});

req.setTimeout(45000, () => {
console.error(`⏰ Request timeout for ${filename}`);
req.destroy();
reject(new Error('Request timeout after 45 seconds'));
});

console.log(`🚀 Sending request to API...`);
req.write(data);
req.end();
});
}

async function processAllFiles() {
const processingDir = '/tmp/doc-processing';
const results = {};

if (!fs.existsSync(processingDir)) {
console.log('No processing directory found');
return results;
}

const metaFiles = fs.readdirSync(processingDir).filter(f => f.endsWith('.meta'));
console.log(`Found ${metaFiles.length} file(s) to process`);

for (const metaFile of metaFiles) {
const metaPath = path.join(processingDir, metaFile);

try {
const metadata = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
console.log(`🔍 Processing: ${metadata.filename}`);
console.log(`📄 Content preview: "${metadata.content.substring(0, 150)}..."`);
console.log(`📏 Content length: ${metadata.content.length} characters`);
console.log(`🔤 Content type: ${typeof metadata.content}`);

const result = await checkDocStyle(metadata.content, metadata.filename);
results[metadata.filename] = result;

const issueCount = result.issues ? result.issues.length : 0;
console.log(`✅ ${metadata.filename}: ${issueCount} issue(s) found`);

await new Promise(resolve => setTimeout(resolve, 3000));

} catch (error) {
console.error(`❌ Error processing ${metaFile}:`, error.message);
console.error(`❌ Error stack:`, error.stack);
const filename = metaFile.replace('.meta', '');
results[filename] = { error: error.message };
}
}

return results;
}

processAllFiles()
.then(results => {
fs.writeFileSync('/tmp/style-check-results.json', JSON.stringify(results, null, 2));
console.log('📊 Style check completed');
})
.catch(error => {
console.error('❌ Style check failed:', error);
process.exit(1);
});
EOF

node check_docs.js

- name: Post PR Comment
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/style-check-results.json', 'utf8');
results = JSON.parse(resultsData);
} catch (error) {
console.log('No results to process');
return;
}

console.log(`Processing results for ${Object.keys(results).length} files`);

let comment = '## 📝 Doc Style Checker Results\n\n';
let hasIssues = false;
let totalIssues = 0;

for (const [filename, result] of Object.entries(results)) {
if (result.error) {
comment += `### ❌ ${filename}\n**Error:** \`${result.error}\`\n\n`;
continue;
}

if (!result.issues || result.issues.length === 0) {
comment += `### ✅ ${filename}\nNo style issues found!\n\n`;
continue;
}

// Process the nested structure from your API
let fileIssues = [];
result.issues.forEach(categoryGroup => {
if (categoryGroup.issues && Array.isArray(categoryGroup.issues)) {
categoryGroup.issues.forEach(issue => {
fileIssues.push({
category: categoryGroup.category,
...issue
});
});
}
});

if (fileIssues.length === 0) {
comment += `### ✅ ${filename}\nNo style issues found!\n\n`;
continue;
}

hasIssues = true;
totalIssues += fileIssues.length;
comment += `### 📋 ${filename}\n**${fileIssues.length} issue${fileIssues.length > 1 ? 's' : ''} found**\n\n`;

// Group by category
const byCategory = {};
fileIssues.forEach(issue => {
const cat = issue.category || 'General';
if (!byCategory[cat]) byCategory[cat] = [];
byCategory[cat].push(issue);
});

for (const [category, issues] of Object.entries(byCategory)) {
comment += `#### ${category}\n\n`;

issues.forEach((issue, i) => {
comment += `<details>\n<summary><strong>Issue ${i + 1}</strong></summary>\n\n`;

if (issue.problem) {
comment += `**Problem:** ${issue.problem}\n\n`;
}

if (issue.problematicText) {
comment += `**Text:**\n\`\`\`\n${issue.problematicText}\n\`\`\`\n\n`;
}

if (issue.location) {
comment += `**Location:** ${issue.location}\n\n`;
}

if (issue.suggestion) {
comment += `**Suggestion:** ${issue.suggestion}\n\n`;
}

if (issue.guideline) {
comment += `**Guideline:** ${issue.guideline}\n\n`;
}

comment += `</details>\n\n`;
});
}
}

if (hasIssues) {
const summary = `🔍 **Summary:** ${totalIssues} issue${totalIssues > 1 ? 's' : ''} found across ${Object.keys(results).length} file${Object.keys(results).length > 1 ? 's' : ''}\n\n`;
comment = comment.replace('## 📝 Doc Style Checker Results\n\n', `## 📝 Doc Style Checker Results\n\n${summary}`);
} else if (Object.keys(results).length > 0) {
comment += '🎉 **All documentation looks great!** No style issues found.\n\n';
}

comment += '---\n*Automated by [Doc Style Checker](https://doc-style-checker.vercel.app/) • Couchbase Documentation Style Guide*';

try {
const [owner, repo] = '${{ inputs.repository }}'.split('/');
await github.rest.issues.createComment({
issue_number: parseInt('${{ inputs.pull_request_number }}'),
owner: owner,
repo: repo,
body: comment
});
console.log('✅ Comment posted successfully');
} catch (error) {
console.error('❌ Failed to post comment:', error);
throw error;
}
19 changes: 19 additions & 0 deletions .github/workflows/docs-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Documentation Review
on:
pull_request:
types: [opened, synchronize, reopened]

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 }}
Loading