From 762e574752d0da053d67b0ab48558b04d29ed6cf Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 16:44:07 -0600 Subject: [PATCH 1/9] docs(ci): add Vale CI integration design Design for adding Vale style linting to GitHub Actions PR checks: - Block merging on style errors - Inline annotations + PR summary comments - Smart config detection per product area - Shared content resolution script (reusable by other workflows) - Docker-based Vale execution (matches local setup) --- docs/plans/2026-02-11-vale-ci-design.md | 190 ++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 docs/plans/2026-02-11-vale-ci-design.md diff --git a/docs/plans/2026-02-11-vale-ci-design.md b/docs/plans/2026-02-11-vale-ci-design.md new file mode 100644 index 0000000000..a2886960d3 --- /dev/null +++ b/docs/plans/2026-02-11-vale-ci-design.md @@ -0,0 +1,190 @@ +# Vale CI Integration Design + +## Overview + +Add Vale style linting to GitHub Actions CI to enforce documentation quality standards on pull requests. The workflow blocks merging on style errors and provides actionable feedback through inline annotations and PR summary comments. + +## Goals + +1. **Block merging on style errors** - PRs with Vale errors cannot be merged +2. **Provide informational feedback** - Warnings and suggestions shown but don't block +3. **Support humans and AI agents** - Structured output that both can act on autonomously + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Scope | Content + instruction files | Match local Lefthook behavior | +| Config handling | Smart detection per file | Respect product-specific branding rules | +| Reporting | Inline annotations + PR comment | Context on lines + overview for agents | +| Filter mode | Added lines only | Avoid noise from pre-existing issues | +| Alert level | Errors block, warnings don't | Match current local behavior | +| Vale installation | Docker (`jdkato/vale:latest`) | Always current, matches local setup | + +## Files to Create + +``` +.github/ +├── scripts/ +│ ├── resolve-shared-content.sh # Shared content resolution +│ └── vale-check.sh # Vale runner with config mapping +└── workflows/ + └── pr-vale-check.yml # Main workflow +``` + +## Workflow Architecture + +``` +pr-vale-check.yml +├── Trigger: pull_request on markdown files +├── Steps: +│ ├── 1. Checkout repository +│ ├── 2. Detect changed files (GitHub API) +│ ├── 3. Skip if no files to check +│ ├── 4. Resolve shared content → product pages +│ ├── 5. Run Vale via Docker (grouped by config) +│ ├── 6. Process results & generate annotations +│ ├── 7. Post PR comment summary +│ ├── 8. Upload results artifact +│ └── 9. Fail if errors found +``` + +## Trigger Paths + +```yaml +on: + pull_request: + paths: + - 'content/**/*.md' + - 'README.md' + - 'DOCS-*.md' + - '**/AGENTS.md' + - '**/CLAUDE.md' + - '.github/**/*.md' + - '.claude/**/*.md' + types: [opened, synchronize, reopened] +``` + +## Shared Content Resolution + +Files in `content/shared/` are sourced by product-specific pages. When a shared file changes, Vale must lint the consuming pages (which have product-specific configs). + +**Script**: `.github/scripts/resolve-shared-content.sh` + +**Logic**: +1. For each changed file: + - If `content/shared/*`: find all pages with `source:` frontmatter pointing to it + - Else: pass through unchanged +2. Output resolved file list + +**Example**: +``` +Input: + content/shared/influxdb3-cli/admin/database/create.md + +Output: + content/influxdb3/core/reference/cli/influxdb3/admin/database/create.md + content/influxdb3/enterprise/reference/cli/influxdb3/admin/database/create.md +``` + +**Reusability**: This script can be used by other workflows (link-checker, tests) that also need shared content resolution. + +## Vale Config Mapping + +**Script**: `.github/scripts/vale-check.sh` + +Maps files to their Vale config based on path: + +| File Pattern | Vale Config | +|--------------|-------------| +| `content/influxdb3/cloud-dedicated/**` | `content/influxdb3/cloud-dedicated/.vale.ini` | +| `content/influxdb3/cloud-serverless/**` | `content/influxdb3/cloud-serverless/.vale.ini` | +| `content/influxdb/v2/**` | `content/influxdb/v2/.vale.ini` | +| `content/**/*.md` (other) | `.vale.ini` | +| Non-content files | `.vale-instructions.ini` | + +Files are grouped by config, then Vale runs once per group to minimize Docker invocations. + +## Vale Execution + +Uses Docker to match local development: + +```bash +docker run --rm \ + -v $(pwd):/workdir \ + -w /workdir \ + jdkato/vale:latest \ + --config=$CONFIG \ + --output=JSON \ + $FILES +``` + +## Output Processing + +### GitHub Annotations + +```bash +# Errors (visible in Files Changed, block merge) +echo "::error file=${FILE},line=${LINE},title=${CHECK}::${MESSAGE}" + +# Warnings (visible, don't block) +echo "::warning file=${FILE},line=${LINE},title=${CHECK}::${MESSAGE}" + +# Suggestions (visible, don't block) +echo "::notice file=${FILE},line=${LINE},title=${CHECK}::${MESSAGE}" +``` + +### PR Summary Comment + +Posted via `github-script` action: + +```markdown +## Vale Style Check Results + +| Metric | Count | +|--------|-------| +| Files checked | 12 | +| Errors | 3 | +| Warnings | 7 | +| Suggestions | 2 | + +### Errors (blocking) + +| File | Line | Rule | Message | +|------|------|------|---------| +| `content/influxdb3/core/get-started.md` | 42 | InfluxDataDocs.Branding | Use 'InfluxDB' instead of 'influxdb' | + +
+Warnings (7) + +| File | Line | Rule | Message | +|------|------|------|---------| +| ... | + +
+ +--- +❌ **Check failed** — fix 3 error(s) before merging. +``` + +## Exit Behavior + +- **Exit 0**: No errors (warnings/suggestions allowed) +- **Exit 1**: One or more errors (blocks merge) +- Summary comment posted in both cases + +## Implementation Tasks + +1. [ ] Create `.github/scripts/resolve-shared-content.sh` +2. [ ] Create `.github/scripts/vale-check.sh` +3. [ ] Create `.github/workflows/pr-vale-check.yml` +4. [ ] Test on a PR with intentional Vale errors +5. [ ] Update `pr-link-check.yml` to use shared resolution script +6. [ ] Document in DOCS-TESTING.md + +## Future Considerations + +- **Caching**: Cache Docker image pull across runs +- **Parallel execution**: Run Vale config groups in parallel if performance becomes an issue +- **Shared workflow**: Consider `workflow_call` if other repos need the same check +- **Remove npm Vale**: Once CI uses Docker, `@vvago/vale` can be removed from devDependencies From 60fb973ae2d263b18643ec230ca418ff2ab134e8 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:05:58 -0600 Subject: [PATCH 2/9] docs(ci): add Vale CI implementation plan Detailed step-by-step plan for implementing Vale CI: - Task 1: Shared content resolution script - Task 2: Vale check script with config mapping - Task 3: GitHub Actions workflow - Task 4: Testing procedure - Task 5: Documentation updates - Task 6: Final verification --- .../2026-02-11-vale-ci-implementation.md | 685 ++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 docs/plans/2026-02-11-vale-ci-implementation.md diff --git a/docs/plans/2026-02-11-vale-ci-implementation.md b/docs/plans/2026-02-11-vale-ci-implementation.md new file mode 100644 index 0000000000..d8ed0a4eaa --- /dev/null +++ b/docs/plans/2026-02-11-vale-ci-implementation.md @@ -0,0 +1,685 @@ +# Vale CI Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add Vale style linting to GitHub Actions that blocks PRs on errors and provides actionable feedback. + +**Architecture:** Single workflow with two supporting scripts. The shared content resolution script expands `content/shared/` files to their consuming product pages. The vale-check script maps files to product-specific Vale configs and runs Vale via Docker. + +**Tech Stack:** Bash scripts, GitHub Actions YAML, Docker (jdkato/vale), jq for JSON processing + +--- + +## Task 1: Create Shared Content Resolution Script + +**Files:** +- Create: `.github/scripts/resolve-shared-content.sh` + +**Step 1: Create the script with basic structure** + +```bash +#!/bin/bash +# Resolve shared content files to their consuming product pages. +# +# Usage: +# echo "content/shared/foo.md" | ./resolve-shared-content.sh +# ./resolve-shared-content.sh < changed_files.txt +# ./resolve-shared-content.sh changed_files.txt +# +# For shared files (content/shared/*), finds all pages with matching +# `source:` frontmatter and outputs those instead. Non-shared files +# pass through unchanged. + +set -euo pipefail + +# Read input from file argument or stdin +if [[ $# -gt 0 && -f "$1" ]]; then + INPUT=$(cat "$1") +else + INPUT=$(cat) +fi + +# Process each file +while IFS= read -r file; do + [[ -z "$file" ]] && continue + + if [[ "$file" == content/shared/* ]]; then + # Extract the shared path portion (e.g., /shared/influxdb3-cli/foo.md) + SHARED_PATH="${file#content}" + + # Find all files that source this shared content + # The source frontmatter looks like: source: /shared/path/to/file.md + CONSUMERS=$(grep -rl "^source: ${SHARED_PATH}$" content/ 2>/dev/null | grep -v '^content/shared/' || true) + + if [[ -n "$CONSUMERS" ]]; then + echo "$CONSUMERS" + else + # No consumers found - output the shared file itself + # (Vale can still lint it with default config) + echo "$file" + fi + else + # Non-shared file - pass through + echo "$file" + fi +done <<< "$INPUT" | sort -u +``` + +**Step 2: Make script executable and commit** + +Run: +```bash +chmod +x .github/scripts/resolve-shared-content.sh +git add .github/scripts/resolve-shared-content.sh +git commit -m "feat(ci): add shared content resolution script + +Resolves content/shared/* files to their consuming product pages +by searching for matching source: frontmatter. Reusable by Vale, +link-checker, and other workflows that need shared content expansion." +``` + +**Step 3: Test the script locally** + +Run: +```bash +# Test with a known shared file +echo "content/shared/influxdb3-cli/admin/database/create.md" | .github/scripts/resolve-shared-content.sh + +# Test with mixed input +printf "content/shared/identify-version.md\ncontent/influxdb3/core/get-started.md" | .github/scripts/resolve-shared-content.sh +``` + +Expected: Shared files expand to product pages, non-shared files pass through. + +--- + +## Task 2: Create Vale Check Script + +**Files:** +- Create: `.github/scripts/vale-check.sh` + +**Step 1: Create the script** + +```bash +#!/bin/bash +# Run Vale on files with appropriate product-specific configs. +# +# Usage: +# ./vale-check.sh file1.md file2.md ... +# ./vale-check.sh < files.txt +# ./vale-check.sh --files files.txt +# +# Groups files by their Vale config, runs Vale once per group, +# and outputs combined JSON results. +# +# Exit codes: +# 0 - No errors (warnings/suggestions allowed) +# 1 - One or more errors found + +set -euo pipefail + +# Parse arguments +FILES=() +while [[ $# -gt 0 ]]; do + case "$1" in + --files) + shift + while IFS= read -r f; do + [[ -n "$f" ]] && FILES+=("$f") + done < "$1" + shift + ;; + *) + FILES+=("$1") + shift + ;; + esac +done + +# Read from stdin if no files provided +if [[ ${#FILES[@]} -eq 0 ]]; then + while IFS= read -r f; do + [[ -n "$f" ]] && FILES+=("$f") + done +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "No files to check" >&2 + exit 0 +fi + +# Map file to Vale config +get_vale_config() { + local file="$1" + case "$file" in + content/influxdb3/cloud-dedicated/*) echo "content/influxdb3/cloud-dedicated/.vale.ini" ;; + content/influxdb3/cloud-serverless/*) echo "content/influxdb3/cloud-serverless/.vale.ini" ;; + content/influxdb/v2/*) echo "content/influxdb/v2/.vale.ini" ;; + content/*) echo ".vale.ini" ;; + *) echo ".vale-instructions.ini" ;; + esac +} + +# Group files by config +declare -A CONFIG_GROUPS +for file in "${FILES[@]}"; do + config=$(get_vale_config "$file") + CONFIG_GROUPS["$config"]+="$file"$'\n' +done + +# Run Vale for each config group and collect results +ALL_RESULTS='{"files": {}, "errors": 0, "warnings": 0, "suggestions": 0}' +TOTAL_ERRORS=0 + +for config in "${!CONFIG_GROUPS[@]}"; do + files="${CONFIG_GROUPS[$config]}" + # Remove trailing newline and convert to array + mapfile -t file_array <<< "${files%$'\n'}" + + echo "Running Vale with config: $config (${#file_array[@]} files)" >&2 + + # Run Vale via Docker + RESULT=$(docker run --rm \ + -v "$(pwd)":/workdir \ + -w /workdir \ + jdkato/vale:latest \ + --config="$config" \ + --output=JSON \ + --minAlertLevel=suggestion \ + "${file_array[@]}" 2>/dev/null || true) + + if [[ -n "$RESULT" && "$RESULT" != "null" ]]; then + # Merge results - Vale outputs {filepath: [alerts...]} + # Count errors in this batch + BATCH_ERRORS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "error")) | length') + BATCH_WARNINGS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "warning")) | length') + BATCH_SUGGESTIONS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "suggestion")) | length') + + TOTAL_ERRORS=$((TOTAL_ERRORS + BATCH_ERRORS)) + + # Merge file results into ALL_RESULTS + ALL_RESULTS=$(echo "$ALL_RESULTS" | jq --argjson new "$RESULT" \ + --argjson e "$BATCH_ERRORS" \ + --argjson w "$BATCH_WARNINGS" \ + --argjson s "$BATCH_SUGGESTIONS" \ + '.files += $new | .errors += $e | .warnings += $w | .suggestions += $s') + fi +done + +# Output combined results +echo "$ALL_RESULTS" + +# Exit with error if any errors found +if [[ $TOTAL_ERRORS -gt 0 ]]; then + exit 1 +fi +exit 0 +``` + +**Step 2: Make script executable and commit** + +Run: +```bash +chmod +x .github/scripts/vale-check.sh +git add .github/scripts/vale-check.sh +git commit -m "feat(ci): add Vale check script with config mapping + +Maps files to product-specific Vale configs matching lefthook.yml: +- cloud-dedicated, cloud-serverless, v2 use product configs +- Other content uses root .vale.ini +- Non-content uses .vale-instructions.ini + +Runs Vale via Docker for version consistency with local dev." +``` + +**Step 3: Test the script locally** + +Run: +```bash +# Test with a content file +echo "content/influxdb3/core/get-started/_index.md" | .github/scripts/vale-check.sh + +# Test with instruction file +echo "README.md" | .github/scripts/vale-check.sh +``` + +Expected: Vale runs with appropriate config, outputs JSON. + +--- + +## Task 3: Create Vale CI Workflow + +**Files:** +- Create: `.github/workflows/pr-vale-check.yml` + +**Step 1: Create the workflow file** + +```yaml +name: Vale Style Check + +on: + pull_request: + paths: + - 'content/**/*.md' + - 'README.md' + - 'DOCS-*.md' + - '**/AGENTS.md' + - '**/CLAUDE.md' + - '.github/**/*.md' + - '.claude/**/*.md' + types: [opened, synchronize, reopened] + +jobs: + vale-check: + name: Vale style check + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Detect changed files + id: detect + run: | + echo "🔍 Detecting changed files in PR #${{ github.event.number }}" + + # Get changed files via GitHub API + curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \ + | jq -r '.[].filename' > all_changed_files.txt + + # Filter for markdown files in scope + grep -E '^(content/.*\.md|README\.md|DOCS-.*\.md|.*/AGENTS\.md|.*/CLAUDE\.md|\.github/.*\.md|\.claude/.*\.md)$' \ + all_changed_files.txt > files_to_check.txt || true + + if [[ -s files_to_check.txt ]]; then + FILE_COUNT=$(wc -l < files_to_check.txt | tr -d ' ') + echo "✅ Found $FILE_COUNT file(s) to check" + echo "has-files=true" >> $GITHUB_OUTPUT + cat files_to_check.txt + else + echo "❌ No matching files to check" + echo "has-files=false" >> $GITHUB_OUTPUT + fi + + - name: Skip if no files to check + if: steps.detect.outputs.has-files == 'false' + run: | + echo "No markdown files in scope - skipping Vale check" + echo "✅ **No files to check** - Vale check skipped" >> $GITHUB_STEP_SUMMARY + + - name: Resolve shared content + if: steps.detect.outputs.has-files == 'true' + run: | + chmod +x .github/scripts/resolve-shared-content.sh + .github/scripts/resolve-shared-content.sh files_to_check.txt > resolved_files.txt + + echo "📁 Resolved files:" + cat resolved_files.txt + + RESOLVED_COUNT=$(wc -l < resolved_files.txt | tr -d ' ') + echo "resolved-count=$RESOLVED_COUNT" >> $GITHUB_OUTPUT + + - name: Run Vale + if: steps.detect.outputs.has-files == 'true' + id: vale + run: | + chmod +x .github/scripts/vale-check.sh + + set +e # Don't exit on error + .github/scripts/vale-check.sh --files resolved_files.txt > vale_results.json 2>vale_stderr.txt + EXIT_CODE=$? + set -e + + # Log stderr for debugging + if [[ -s vale_stderr.txt ]]; then + echo "Vale output:" + cat vale_stderr.txt + fi + + # Parse results + if [[ -s vale_results.json ]]; then + ERRORS=$(jq -r '.errors // 0' vale_results.json) + WARNINGS=$(jq -r '.warnings // 0' vale_results.json) + SUGGESTIONS=$(jq -r '.suggestions // 0' vale_results.json) + + echo "error-count=$ERRORS" >> $GITHUB_OUTPUT + echo "warning-count=$WARNINGS" >> $GITHUB_OUTPUT + echo "suggestion-count=$SUGGESTIONS" >> $GITHUB_OUTPUT + + if [[ $ERRORS -gt 0 ]]; then + echo "check-result=failed" >> $GITHUB_OUTPUT + else + echo "check-result=passed" >> $GITHUB_OUTPUT + fi + else + echo "check-result=error" >> $GITHUB_OUTPUT + echo "error-count=0" >> $GITHUB_OUTPUT + echo "warning-count=0" >> $GITHUB_OUTPUT + echo "suggestion-count=0" >> $GITHUB_OUTPUT + fi + + exit $EXIT_CODE + + - name: Generate annotations + if: always() && steps.detect.outputs.has-files == 'true' && steps.vale.outputs.check-result != '' + run: | + if [[ ! -s vale_results.json ]]; then + echo "No results to process" + exit 0 + fi + + # Process each file's alerts + jq -r '.files | to_entries[] | .key as $file | .value[] | "\($file)|\(.Line)|\(.Severity)|\(.Check)|\(.Message)"' \ + vale_results.json 2>/dev/null | while IFS='|' read -r file line severity check message; do + + case "$severity" in + error) + echo "::error file=${file},line=${line},title=${check}::${message}" + ;; + warning) + echo "::warning file=${file},line=${line},title=${check}::${message}" + ;; + suggestion) + echo "::notice file=${file},line=${line},title=${check}::${message}" + ;; + esac + done + + - name: Post PR comment + if: always() && steps.detect.outputs.has-files == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read results + let results = { files: {}, errors: 0, warnings: 0, suggestions: 0 }; + try { + results = JSON.parse(fs.readFileSync('vale_results.json', 'utf8')); + } catch (e) { + console.log('Could not read results:', e.message); + } + + const errors = results.errors || 0; + const warnings = results.warnings || 0; + const suggestions = results.suggestions || 0; + const checkResult = '${{ steps.vale.outputs.check-result }}'; + + // Build comment body + let body = '## Vale Style Check Results\n\n'; + body += '| Metric | Count |\n'; + body += '|--------|-------|\n'; + body += `| Errors | ${errors} |\n`; + body += `| Warnings | ${warnings} |\n`; + body += `| Suggestions | ${suggestions} |\n\n`; + + // List errors (blocking) + if (errors > 0) { + body += '### Errors (blocking)\n\n'; + body += '| File | Line | Rule | Message |\n'; + body += '|------|------|------|--------|\n'; + + for (const [file, alerts] of Object.entries(results.files || {})) { + for (const alert of alerts) { + if (alert.Severity === 'error') { + const msg = alert.Message.replace(/\|/g, '\\|').substring(0, 100); + body += `| \`${file}\` | ${alert.Line} | ${alert.Check} | ${msg} |\n`; + } + } + } + body += '\n'; + } + + // Collapsible warnings + if (warnings > 0) { + body += '
\n'; + body += `Warnings (${warnings})\n\n`; + body += '| File | Line | Rule | Message |\n'; + body += '|------|------|------|--------|\n'; + + let count = 0; + for (const [file, alerts] of Object.entries(results.files || {})) { + for (const alert of alerts) { + if (alert.Severity === 'warning' && count < 20) { + const msg = alert.Message.replace(/\|/g, '\\|').substring(0, 100); + body += `| \`${file}\` | ${alert.Line} | ${alert.Check} | ${msg} |\n`; + count++; + } + } + } + if (warnings > 20) { + body += `\n_Showing first 20 of ${warnings} warnings._\n`; + } + body += '\n
\n\n'; + } + + // Status line + body += '---\n'; + if (checkResult === 'failed') { + body += `❌ **Check failed** — fix ${errors} error(s) before merging.\n`; + } else if (checkResult === 'passed') { + body += '✅ **Check passed**\n'; + } else { + body += '⚠️ **Check could not complete**\n'; + } + + // Find and update existing comment or create new + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('## Vale Style Check Results') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Write step summary + if: always() && steps.detect.outputs.has-files == 'true' + run: | + ERRORS="${{ steps.vale.outputs.error-count }}" + WARNINGS="${{ steps.vale.outputs.warning-count }}" + SUGGESTIONS="${{ steps.vale.outputs.suggestion-count }}" + RESULT="${{ steps.vale.outputs.check-result }}" + + echo "## Vale Style Check Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Errors | ${ERRORS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "| Warnings | ${WARNINGS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "| Suggestions | ${SUGGESTIONS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$RESULT" == "failed" ]]; then + echo "❌ **Check failed** — fix ${ERRORS} error(s) before merging." >> $GITHUB_STEP_SUMMARY + elif [[ "$RESULT" == "passed" ]]; then + echo "✅ **Check passed**" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ **Check could not complete**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Upload results + if: always() && steps.detect.outputs.has-files == 'true' + uses: actions/upload-artifact@v4 + with: + name: vale-results + path: | + vale_results.json + resolved_files.txt + files_to_check.txt + retention-days: 30 + + - name: Fail on errors + if: steps.vale.outputs.check-result == 'failed' + run: | + echo "Vale found ${{ steps.vale.outputs.error-count }} error(s)" + exit 1 +``` + +**Step 2: Commit the workflow** + +Run: +```bash +git add .github/workflows/pr-vale-check.yml +git commit -m "feat(ci): add Vale style check workflow + +Runs Vale on PR changes with product-specific configs: +- Blocks merging on errors +- Shows warnings/suggestions as annotations +- Posts summary comment for humans and AI agents +- Uses Docker for Vale version consistency" +``` + +--- + +## Task 4: Test the Workflow + +**Step 1: Create a test file with intentional errors** + +Create `content/influxdb3/core/test-vale-errors.md`: +```markdown +--- +title: Test Vale Errors +description: Temporary test file for Vale CI. +--- + +This file tests the vale CI workflow. + +Here is an error: influxdb should be InfluxDB. + +Here is a branding issue: influx-db is wrong. +``` + +**Step 2: Commit and push to trigger workflow** + +Run: +```bash +git add content/influxdb3/core/test-vale-errors.md +git commit -m "test(ci): add temporary file to test Vale workflow" +git push origin docs-v2-jts-vale-ci +``` + +**Step 3: Open a PR and verify** + +1. Open PR from `docs-v2-jts-vale-ci` to `master` +2. Verify Vale check runs +3. Verify annotations appear on changed lines +4. Verify PR comment is posted +5. Verify check fails due to errors + +**Step 4: Clean up test file** + +Run: +```bash +git rm content/influxdb3/core/test-vale-errors.md +git commit -m "test(ci): remove Vale test file" +git push origin docs-v2-jts-vale-ci +``` + +--- + +## Task 5: Update Documentation + +**Files:** +- Modify: `DOCS-TESTING.md` + +**Step 1: Add Vale CI section to DOCS-TESTING.md** + +Find the "Style Linting (Vale)" section and add after the existing content: + +```markdown +### CI Integration + +Vale runs automatically on pull requests that modify markdown files. The workflow: + +1. Detects changed markdown files (content, README, instruction files) +2. Resolves shared content to consuming product pages +3. Maps files to appropriate Vale configs (matching local Lefthook behavior) +4. Runs Vale via Docker (`jdkato/vale:latest`) +5. Reports results as inline annotations and a PR summary comment + +**Alert levels:** +- **Errors** block merging +- **Warnings** and **suggestions** are informational only + +**Files checked:** +- `content/**/*.md` +- `README.md`, `DOCS-*.md` +- `**/AGENTS.md`, `**/CLAUDE.md` +- `.github/**/*.md`, `.claude/**/*.md` + +The CI check uses the same product-specific configs as local development, ensuring consistency between local and CI linting. +``` + +**Step 2: Commit documentation update** + +Run: +```bash +git add DOCS-TESTING.md +git commit -m "docs: add Vale CI section to DOCS-TESTING.md" +``` + +--- + +## Task 6: Final Verification + +**Step 1: Verify all scripts are executable** + +Run: +```bash +ls -la .github/scripts/resolve-shared-content.sh +ls -la .github/scripts/vale-check.sh +``` + +Expected: Both show `-rwxr-xr-x` permissions. + +**Step 2: Verify workflow syntax** + +Run: +```bash +# Use actionlint if available, or just check YAML validity +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/pr-vale-check.yml'))" +``` + +Expected: No errors. + +**Step 3: Push final changes** + +Run: +```bash +git push origin docs-v2-jts-vale-ci +``` + +--- + +## Summary + +After completing all tasks, you will have: + +1. `.github/scripts/resolve-shared-content.sh` - Reusable script for expanding shared content +2. `.github/scripts/vale-check.sh` - Vale runner with config mapping +3. `.github/workflows/pr-vale-check.yml` - CI workflow with annotations and PR comments +4. Updated `DOCS-TESTING.md` with CI documentation + +The workflow mirrors local Lefthook behavior, using Docker-based Vale and product-specific configs. From bfc03ce5eeecef74c24f207fe67791d61e481caa Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:09:19 -0600 Subject: [PATCH 3/9] feat(ci): add shared content resolution script Resolves content/shared/* files to their consuming product pages by searching for matching source: frontmatter. Reusable by Vale, link-checker, and other workflows that need shared content expansion. --- .github/scripts/resolve-shared-content.sh | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100755 .github/scripts/resolve-shared-content.sh diff --git a/.github/scripts/resolve-shared-content.sh b/.github/scripts/resolve-shared-content.sh new file mode 100755 index 0000000000..db3a23f129 --- /dev/null +++ b/.github/scripts/resolve-shared-content.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Resolve shared content files to their consuming product pages. +# +# Usage: +# echo "content/shared/foo.md" | ./resolve-shared-content.sh +# ./resolve-shared-content.sh < changed_files.txt +# ./resolve-shared-content.sh changed_files.txt +# +# For shared files (content/shared/*), finds all pages with matching +# `source:` frontmatter and outputs those instead. Non-shared files +# pass through unchanged. + +set -euo pipefail + +# Read input from file argument or stdin +if [[ $# -gt 0 && -f "$1" ]]; then + INPUT=$(cat "$1") +else + INPUT=$(cat) +fi + +# Process each file +while IFS= read -r file; do + [[ -z "$file" ]] && continue + + if [[ "$file" == content/shared/* ]]; then + # Extract the shared path portion (e.g., /shared/influxdb3-cli/foo.md) + SHARED_PATH="${file#content}" + + # Find all files that source this shared content + # The source frontmatter looks like: source: /shared/path/to/file.md + CONSUMERS=$(grep -rl "^source: ${SHARED_PATH}$" content/ 2>/dev/null | grep -v '^content/shared/' || true) + + if [[ -n "$CONSUMERS" ]]; then + echo "$CONSUMERS" + else + # No consumers found - output the shared file itself + # (Vale can still lint it with default config) + echo "$file" + fi + else + # Non-shared file - pass through + echo "$file" + fi +done <<< "$INPUT" | sort -u From 205800acc221d63d6231f2a6f2d9d6eff98b66c1 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:11:13 -0600 Subject: [PATCH 4/9] feat(ci): add Vale check script with config mapping Maps files to product-specific Vale configs matching lefthook.yml: - cloud-dedicated, cloud-serverless, v2 use product configs - Other content uses root .vale.ini - Non-content uses .vale-instructions.ini Runs Vale via Docker for version consistency with local dev. --- .github/scripts/vale-check.sh | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100755 .github/scripts/vale-check.sh diff --git a/.github/scripts/vale-check.sh b/.github/scripts/vale-check.sh new file mode 100755 index 0000000000..0b61aa949a --- /dev/null +++ b/.github/scripts/vale-check.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# Run Vale on files with appropriate product-specific configs. +# +# Usage: +# ./vale-check.sh file1.md file2.md ... +# ./vale-check.sh < files.txt +# ./vale-check.sh --files files.txt +# +# Groups files by their Vale config, runs Vale once per group, +# and outputs combined JSON results. +# +# Exit codes: +# 0 - No errors (warnings/suggestions allowed) +# 1 - One or more errors found + +set -euo pipefail + +# Parse arguments +FILES=() +while [[ $# -gt 0 ]]; do + case "$1" in + --files) + shift + while IFS= read -r f; do + [[ -n "$f" ]] && FILES+=("$f") + done < "$1" + shift + ;; + *) + FILES+=("$1") + shift + ;; + esac +done + +# Read from stdin if no files provided +if [[ ${#FILES[@]} -eq 0 ]]; then + while IFS= read -r f; do + [[ -n "$f" ]] && FILES+=("$f") + done +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "No files to check" >&2 + exit 0 +fi + +# Map file to Vale config +get_vale_config() { + local file="$1" + case "$file" in + content/influxdb3/cloud-dedicated/*) echo "content/influxdb3/cloud-dedicated/.vale.ini" ;; + content/influxdb3/cloud-serverless/*) echo "content/influxdb3/cloud-serverless/.vale.ini" ;; + content/influxdb/v2/*) echo "content/influxdb/v2/.vale.ini" ;; + content/*) echo ".vale.ini" ;; + *) echo ".vale-instructions.ini" ;; + esac +} + +# Group files by config +declare -A CONFIG_GROUPS +for file in "${FILES[@]}"; do + config=$(get_vale_config "$file") + CONFIG_GROUPS["$config"]+="$file"$'\n' +done + +# Run Vale for each config group and collect results +ALL_RESULTS='{"files": {}, "errors": 0, "warnings": 0, "suggestions": 0}' +TOTAL_ERRORS=0 + +for config in "${!CONFIG_GROUPS[@]}"; do + files="${CONFIG_GROUPS[$config]}" + # Remove trailing newline and convert to array + mapfile -t file_array <<< "${files%$'\n'}" + + echo "Running Vale with config: $config (${#file_array[@]} files)" >&2 + + # Run Vale via Docker + RESULT=$(docker run --rm \ + -v "$(pwd)":/workdir \ + -w /workdir \ + jdkato/vale:latest \ + --config="$config" \ + --output=JSON \ + --minAlertLevel=suggestion \ + "${file_array[@]}" 2>/dev/null || true) + + if [[ -n "$RESULT" && "$RESULT" != "null" ]]; then + # Merge results - Vale outputs {filepath: [alerts...]} + # Count errors in this batch + BATCH_ERRORS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "error")) | length') + BATCH_WARNINGS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "warning")) | length') + BATCH_SUGGESTIONS=$(echo "$RESULT" | jq '[.[][].Severity] | map(select(. == "suggestion")) | length') + + TOTAL_ERRORS=$((TOTAL_ERRORS + BATCH_ERRORS)) + + # Merge file results into ALL_RESULTS + ALL_RESULTS=$(echo "$ALL_RESULTS" | jq --argjson new "$RESULT" \ + --argjson e "$BATCH_ERRORS" \ + --argjson w "$BATCH_WARNINGS" \ + --argjson s "$BATCH_SUGGESTIONS" \ + '.files += $new | .errors += $e | .warnings += $w | .suggestions += $s') + fi +done + +# Output combined results +echo "$ALL_RESULTS" + +# Exit with error if any errors found +if [[ $TOTAL_ERRORS -gt 0 ]]; then + exit 1 +fi +exit 0 From af941512605a544de4aa1b679fd591140db651fe Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:14:47 -0600 Subject: [PATCH 5/9] feat(ci): add Vale style check workflow Runs Vale on PR changes with product-specific configs: - Blocks merging on errors - Shows warnings/suggestions as annotations - Posts summary comment for humans and AI agents - Uses Docker for Vale version consistency --- .github/workflows/pr-vale-check.yml | 280 ++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/workflows/pr-vale-check.yml diff --git a/.github/workflows/pr-vale-check.yml b/.github/workflows/pr-vale-check.yml new file mode 100644 index 0000000000..de4c2a19d0 --- /dev/null +++ b/.github/workflows/pr-vale-check.yml @@ -0,0 +1,280 @@ +name: Vale Style Check + +on: + pull_request: + paths: + - 'content/**/*.md' + - 'README.md' + - 'DOCS-*.md' + - '**/AGENTS.md' + - '**/CLAUDE.md' + - '.github/**/*.md' + - '.claude/**/*.md' + types: [opened, synchronize, reopened] + +jobs: + vale-check: + name: Vale style check + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Detect changed files + id: detect + run: | + echo "🔍 Detecting changed files in PR #${{ github.event.number }}" + + # Get changed files via GitHub API + curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \ + | jq -r '.[].filename' > all_changed_files.txt + + # Filter for markdown files in scope + grep -E '^(content/.*\.md|README\.md|DOCS-.*\.md|.*/AGENTS\.md|.*/CLAUDE\.md|\.github/.*\.md|\.claude/.*\.md)$' \ + all_changed_files.txt > files_to_check.txt || true + + if [[ -s files_to_check.txt ]]; then + FILE_COUNT=$(wc -l < files_to_check.txt | tr -d ' ') + echo "✅ Found $FILE_COUNT file(s) to check" + echo "has-files=true" >> $GITHUB_OUTPUT + cat files_to_check.txt + else + echo "❌ No matching files to check" + echo "has-files=false" >> $GITHUB_OUTPUT + fi + + - name: Skip if no files to check + if: steps.detect.outputs.has-files == 'false' + run: | + echo "No markdown files in scope - skipping Vale check" + echo "✅ **No files to check** - Vale check skipped" >> $GITHUB_STEP_SUMMARY + + - name: Resolve shared content + if: steps.detect.outputs.has-files == 'true' + run: | + chmod +x .github/scripts/resolve-shared-content.sh + .github/scripts/resolve-shared-content.sh files_to_check.txt > resolved_files.txt + + echo "📁 Resolved files:" + cat resolved_files.txt + + RESOLVED_COUNT=$(wc -l < resolved_files.txt | tr -d ' ') + echo "resolved-count=$RESOLVED_COUNT" >> $GITHUB_OUTPUT + + - name: Run Vale + if: steps.detect.outputs.has-files == 'true' + id: vale + run: | + chmod +x .github/scripts/vale-check.sh + + set +e # Don't exit on error + .github/scripts/vale-check.sh --files resolved_files.txt > vale_results.json 2>vale_stderr.txt + EXIT_CODE=$? + set -e + + # Log stderr for debugging + if [[ -s vale_stderr.txt ]]; then + echo "Vale output:" + cat vale_stderr.txt + fi + + # Parse results + if [[ -s vale_results.json ]]; then + ERRORS=$(jq -r '.errors // 0' vale_results.json) + WARNINGS=$(jq -r '.warnings // 0' vale_results.json) + SUGGESTIONS=$(jq -r '.suggestions // 0' vale_results.json) + + echo "error-count=$ERRORS" >> $GITHUB_OUTPUT + echo "warning-count=$WARNINGS" >> $GITHUB_OUTPUT + echo "suggestion-count=$SUGGESTIONS" >> $GITHUB_OUTPUT + + if [[ $ERRORS -gt 0 ]]; then + echo "check-result=failed" >> $GITHUB_OUTPUT + else + echo "check-result=passed" >> $GITHUB_OUTPUT + fi + else + echo "check-result=error" >> $GITHUB_OUTPUT + echo "error-count=0" >> $GITHUB_OUTPUT + echo "warning-count=0" >> $GITHUB_OUTPUT + echo "suggestion-count=0" >> $GITHUB_OUTPUT + fi + + exit $EXIT_CODE + + - name: Generate annotations + if: always() && steps.detect.outputs.has-files == 'true' && steps.vale.outputs.check-result != '' + run: | + if [[ ! -s vale_results.json ]]; then + echo "No results to process" + exit 0 + fi + + # Process each file's alerts + jq -r '.files | to_entries[] | .key as $file | .value[] | "\($file)|\(.Line)|\(.Severity)|\(.Check)|\(.Message)"' \ + vale_results.json 2>/dev/null | while IFS='|' read -r file line severity check message; do + + case "$severity" in + error) + echo "::error file=${file},line=${line},title=${check}::${message}" + ;; + warning) + echo "::warning file=${file},line=${line},title=${check}::${message}" + ;; + suggestion) + echo "::notice file=${file},line=${line},title=${check}::${message}" + ;; + esac + done + + - name: Post PR comment + if: always() && steps.detect.outputs.has-files == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read results + let results = { files: {}, errors: 0, warnings: 0, suggestions: 0 }; + try { + results = JSON.parse(fs.readFileSync('vale_results.json', 'utf8')); + } catch (e) { + console.log('Could not read results:', e.message); + } + + const errors = results.errors || 0; + const warnings = results.warnings || 0; + const suggestions = results.suggestions || 0; + const checkResult = '${{ steps.vale.outputs.check-result }}'; + + // Build comment body + let body = '## Vale Style Check Results\n\n'; + body += '| Metric | Count |\n'; + body += '|--------|-------|\n'; + body += `| Errors | ${errors} |\n`; + body += `| Warnings | ${warnings} |\n`; + body += `| Suggestions | ${suggestions} |\n\n`; + + // List errors (blocking) + if (errors > 0) { + body += '### Errors (blocking)\n\n'; + body += '| File | Line | Rule | Message |\n'; + body += '|------|------|------|--------|\n'; + + for (const [file, alerts] of Object.entries(results.files || {})) { + for (const alert of alerts) { + if (alert.Severity === 'error') { + const msg = alert.Message.replace(/\|/g, '\\|').substring(0, 100); + body += `| \`${file}\` | ${alert.Line} | ${alert.Check} | ${msg} |\n`; + } + } + } + body += '\n'; + } + + // Collapsible warnings + if (warnings > 0) { + body += '
\n'; + body += `Warnings (${warnings})\n\n`; + body += '| File | Line | Rule | Message |\n'; + body += '|------|------|------|--------|\n'; + + let count = 0; + for (const [file, alerts] of Object.entries(results.files || {})) { + for (const alert of alerts) { + if (alert.Severity === 'warning' && count < 20) { + const msg = alert.Message.replace(/\|/g, '\\|').substring(0, 100); + body += `| \`${file}\` | ${alert.Line} | ${alert.Check} | ${msg} |\n`; + count++; + } + } + } + if (warnings > 20) { + body += `\n_Showing first 20 of ${warnings} warnings._\n`; + } + body += '\n
\n\n'; + } + + // Status line + body += '---\n'; + if (checkResult === 'failed') { + body += `❌ **Check failed** — fix ${errors} error(s) before merging.\n`; + } else if (checkResult === 'passed') { + body += '✅ **Check passed**\n'; + } else { + body += '⚠️ **Check could not complete**\n'; + } + + // Find and update existing comment or create new + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('## Vale Style Check Results') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Write step summary + if: always() && steps.detect.outputs.has-files == 'true' + run: | + ERRORS="${{ steps.vale.outputs.error-count }}" + WARNINGS="${{ steps.vale.outputs.warning-count }}" + SUGGESTIONS="${{ steps.vale.outputs.suggestion-count }}" + RESULT="${{ steps.vale.outputs.check-result }}" + + echo "## Vale Style Check Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Errors | ${ERRORS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "| Warnings | ${WARNINGS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "| Suggestions | ${SUGGESTIONS:-0} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$RESULT" == "failed" ]]; then + echo "❌ **Check failed** — fix ${ERRORS} error(s) before merging." >> $GITHUB_STEP_SUMMARY + elif [[ "$RESULT" == "passed" ]]; then + echo "✅ **Check passed**" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ **Check could not complete**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Upload results + if: always() && steps.detect.outputs.has-files == 'true' + uses: actions/upload-artifact@v4 + with: + name: vale-results + path: | + vale_results.json + resolved_files.txt + files_to_check.txt + retention-days: 30 + + - name: Fail on errors + if: steps.vale.outputs.check-result == 'failed' + run: | + echo "Vale found ${{ steps.vale.outputs.error-count }} error(s)" + exit 1 From c7c2939ce9daee148aac8280bb3ce9b38291470d Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:15:53 -0600 Subject: [PATCH 6/9] test(ci): add temporary file to test Vale workflow --- content/influxdb3/core/test-vale-errors.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 content/influxdb3/core/test-vale-errors.md diff --git a/content/influxdb3/core/test-vale-errors.md b/content/influxdb3/core/test-vale-errors.md new file mode 100644 index 0000000000..455e9194f1 --- /dev/null +++ b/content/influxdb3/core/test-vale-errors.md @@ -0,0 +1,10 @@ +--- +title: Test Vale Errors +description: Temporary test file for Vale CI. +--- + +This file tests the vale CI workflow. + +Here is an error: influxdb should be InfluxDB. + +Here is a branding issue: influx-db is wrong. From ae9026794002dd335c60e7f1eb90e50ed8e9b909 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:19:37 -0600 Subject: [PATCH 7/9] test(ci): remove Vale test file --- content/influxdb3/core/test-vale-errors.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 content/influxdb3/core/test-vale-errors.md diff --git a/content/influxdb3/core/test-vale-errors.md b/content/influxdb3/core/test-vale-errors.md deleted file mode 100644 index 455e9194f1..0000000000 --- a/content/influxdb3/core/test-vale-errors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Test Vale Errors -description: Temporary test file for Vale CI. ---- - -This file tests the vale CI workflow. - -Here is an error: influxdb should be InfluxDB. - -Here is a branding issue: influx-db is wrong. From 72c87526c637e375ea1c59aa1ba89f0bfc538e79 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 17:20:25 -0600 Subject: [PATCH 8/9] docs: add Vale CI section to DOCS-TESTING.md --- DOCS-TESTING.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/DOCS-TESTING.md b/DOCS-TESTING.md index 341fff3399..d9267d69ec 100644 --- a/DOCS-TESTING.md +++ b/DOCS-TESTING.md @@ -614,6 +614,28 @@ Vale can raise different alert levels: For more configuration details, see [Vale configuration](https://vale.sh/docs/topics/config). +### CI Integration + +Vale runs automatically on pull requests that modify markdown files. The workflow: + +1. Detects changed markdown files (content, README, instruction files) +2. Resolves shared content to consuming product pages +3. Maps files to appropriate Vale configs (matching local Lefthook behavior) +4. Runs Vale via Docker (`jdkato/vale:latest`) +5. Reports results as inline annotations and a PR summary comment + +**Alert levels:** +- **Errors** block merging +- **Warnings** and **suggestions** are informational only + +**Files checked:** +- `content/**/*.md` +- `README.md`, `DOCS-*.md` +- `**/AGENTS.md`, `**/CLAUDE.md` +- `.github/**/*.md`, `.claude/**/*.md` + +The CI check uses the same product-specific configs as local development, ensuring consistency between local and CI linting. + ## Pre-commit Hooks docs-v2 uses [Lefthook](https://github.com/evilmartians/lefthook) to manage Git hooks that run automatically during pre-commit and pre-push. From bd593d5a711667169d5af05f98769faabf1613c7 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 11 Feb 2026 18:10:28 -0600 Subject: [PATCH 9/9] fix(ci): handle files without trailing newline in vale-check.sh Add `|| [[ -n "$f" ]]` to while read loops to catch the last line even when input doesn't end with a newline. This is a common bash pitfall where `while read` skips the final line without a terminator. --- .github/scripts/vale-check.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/vale-check.sh b/.github/scripts/vale-check.sh index 0b61aa949a..d24f3b611f 100755 --- a/.github/scripts/vale-check.sh +++ b/.github/scripts/vale-check.sh @@ -21,7 +21,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --files) shift - while IFS= read -r f; do + while IFS= read -r f || [[ -n "$f" ]]; do [[ -n "$f" ]] && FILES+=("$f") done < "$1" shift @@ -35,7 +35,7 @@ done # Read from stdin if no files provided if [[ ${#FILES[@]} -eq 0 ]]; then - while IFS= read -r f; do + while IFS= read -r f || [[ -n "$f" ]]; do [[ -n "$f" ]] && FILES+=("$f") done fi