diff --git a/.github/workflows/doc-style-checker.yml b/.github/workflows/doc-style-checker.yml new file mode 100644 index 0000000..fa44200 --- /dev/null +++ b/.github/workflows/doc-style-checker.yml @@ -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<> $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); + } + } + } + } \ No newline at end of file diff --git a/.github/workflows/docs-review.yml b/.github/workflows/docs-review.yml new file mode 100644 index 0000000..7873236 --- /dev/null +++ b/.github/workflows/docs-review.yml @@ -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 }} \ No newline at end of file diff --git a/.github/workflows/fi-test.yml b/.github/workflows/fi-test.yml deleted file mode 100644 index ff778e3..0000000 --- a/.github/workflows/fi-test.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Test workflow for FI - -on: - workflow_call: - workflow_dispatch: - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: test - run: | - echo worlld - - diff --git a/test-doc.adoc b/test-doc.adoc new file mode 100644 index 0000000..5649b81 --- /dev/null +++ b/test-doc.adoc @@ -0,0 +1,110 @@ +Data moeeldelling + +couchbase Sync Gateway’s data model; for secure cloud-to-edge synchronization of enterprise data. + +introduction + +This page includes guidance and constraints relating to the design of data buckets and documents that you want to replicate using Sync Gateway. They do not necessarily align with constraints on the local storage and use of such documents. + +property naming + +You can use an underscore prefix (_, ASCII _) for property naming, but your name cannot match any of the Document system properties reserved by Sync Gateway: + +_sync + +_id + +_rev + +_deleted + +_attachments + +_revisions + +_exp + +_purged + +_removed + +Any document that matches the reserved property names listed will be rejected by Sync Gateway — see Example 1 for the error details. + +Example 1. Property prefix error message +text +Copy +"{"error":"Bad Request","reason":"user defined top level properties beginning with '_' are not allowed in document body"}" +Where it applies +This rule applies to writes performed through: + +Couchbase Lite SDKs + +Sync Gateway REST APIs + +Couchbase Server SDKs when shared bucket access is enabled. + +When you might encounter the error +You may encounter the error in the following deployment situations: + +In Mobile-to-Web Data Sync with Node.js Server SDK and Ottoman.js (the Node.js ODM for Couchbase), where the rule conflicts with the _type property that is automatically added by Ottoman.js. + +A suggested workaround in this scenario is to fork the Ottoman.js library, perform a search-replace for the _type property and replace it without a leading underscore. + +For versions 2.x of Sync Gateway, you can encounter the following error: + +In Mobile-to-Web Data Sync with Field-level Encryption enabled, because the rule conflicts with the default field encryption format. + +How to avoid the error +You should change any top-level user properties that have a key with a leading underscore , by either: + +Renaming them to remove the underscore, or, + +Wrapping them inside another object with a key that doesn’t have a leading underscore. + +Document Structure +Couchbase’s unit of data is a document, this is the NOSQL equivalent of a row or record. + +Documents are stored as a key-value pair, which comprises a unique and immutable key, the Id, and a value representing the users' data (a JSON-object or binary blob). + +Key +The document key, the Id, is: + +A UTF-8 string with no spaces, although it may contain special characters, such as (, %, /, ", and _ + +No longer than 250 bytes + +Unique within the bucket + +Automatically generated (as a UUID) or be set by the user or application when saved + +Immutable; that is, once saved the Id cannot be changed. + +Value +The document value is either: + +A JSON value, termed a Document. + +This JSON object is a collection of key/value pairs. The values may be numbers, strings, arrays, or even nested objects. As a result, documents can represent complex data structures in a readily parsable and self-organizing manner. + +a binary object (also known as a blob or attachment) + +These attachments provide a means to store large media files or any other non-textual data. Couchbase Lite supports attachments of unlimited size, although the Sync Gateway imposes a 20MB limit for attachments synced to it. + +Document Attributes +Each Document has the following attributes: + +A document ID + +A current revision ID (which changes when the document is updated) + +A history of past revision IDs (usually linear, but will form a branching tree if the document has or has had conflicts) + +A body in the form of a JSON object (a set of key/value pairs) + +Zero or more named binary attachments + +Document Change History +Couchbase Lite tracks the change history of every document as a series of revisions, like version control systems such as Git or Subversion. Its main purpose is to enable the replicator to determine which data to sync and any conflicts arising. + +Each document change is assigned a unique revision ID. The IDs of past revisions are available. The content of past revisions may be available if the revision was created locally and the database has not yet been compacted. +