diff --git a/.github/actions/style-guide-checker/README.md b/.github/actions/style-guide-checker/README.md new file mode 100644 index 0000000..99ae9ff --- /dev/null +++ b/.github/actions/style-guide-checker/README.md @@ -0,0 +1,351 @@ +# Style Guide Checker Action + +A GitHub Action that uses AI to review QuantEcon lectures for compliance with the [QuantEcon Style Guide](../../copilot-qe-style-guide.md) and automatically suggests or applies improvements. + +## Features + +- ๐Ÿค– **AI-Powered Analysis**: Uses OpenAI GPT-4 to understand style guide context and provide intelligent suggestions +- ๐Ÿ“ **Two Operating Modes**: + - **PR Mode**: Reviews only changed files in pull requests + - **Full Mode**: Reviews all files in the repository (for scheduled runs) +- ๐ŸŽฏ **Confidence-Based Actions**: + - **PR Mode**: + - High-confidence suggestions on changed lines use GitHub's suggestion feature (click to apply) + - Suggestions on unchanged lines are provided as regular comments + - All suggestions are transparent and require reviewer approval + - **Full Mode**: High-confidence changes are auto-committed; others become review suggestions +- ๐Ÿšซ **File Exclusion**: Supports regex patterns to exclude files from review +- ๐Ÿ“Š **Detailed Reporting**: Provides comprehensive summaries and metrics + +## Usage + +### Comment-Based Trigger (Recommended) + +The most user-friendly way to use this action is with comment-based triggering. Users can trigger style guide reviews by commenting on a PR with specific trigger phrases: + +```yaml +name: Style Guide Review (Comment Triggered) +on: + issue_comment: + types: [created] + +jobs: + check-trigger: + if: github.event.issue.pull_request && (contains(github.event.comment.body, '/style-check') || contains(github.event.comment.body, '@quantecon-style-guide')) + runs-on: ubuntu-latest + name: Parse Comment Trigger + outputs: + should-run: ${{ steps.parse.outputs.should-run }} + steps: + - name: Parse comment for trigger phrases + id: parse + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + if echo "$COMMENT_BODY" | grep -q "/style-check\|@quantecon-style-guide"; then + echo "should-run=true" >> $GITHUB_OUTPUT + else + echo "should-run=false" >> $GITHUB_OUTPUT + fi + + style-check: + needs: check-trigger + if: needs.check-trigger.outputs.should-run == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout PR + uses: actions/checkout@v4 + with: + ref: ${{ format('refs/pull/{0}/head', github.event.issue.number) }} + + - name: Run style guide checker + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' +``` + +**Trigger phrases:** +- `/style-check` - Simple command-style trigger +- `@quantecon-style-guide` - Mention-style trigger + +Users can comment either phrase on any PR to trigger a style guide review. + +### Automatic PR Review + +```yaml +name: Style Guide Review (Automatic) +on: + pull_request: + paths: + - 'lectures/**/*.md' + +jobs: + style-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run style guide checker + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' +``` + +### Scheduled Full Review + +```yaml +name: Weekly Style Review +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 9 AM UTC + +jobs: + full-style-review: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run full style guide review + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'full' + create-pr: 'true' +``` + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `style-guide` | Path to the style guide document (local path or URL) | No | `.github/copilot-qe-style-guide.md` | +| `docs` | Path to the documents/lectures directory | No | `lectures` | +| `exclude-files` | Comma-separated list of file patterns to exclude (supports regex) | No | `''` | +| `mode` | Operating mode: `pr` for PR review, `full` for complete review | No | `pr` | +| `confidence-threshold` | Minimum confidence level for auto-commits (`high`, `medium`, `low`) | No | `high` | +| `github-token` | GitHub token for API access | **Yes** | | +| `openai-api-key` | OpenAI API key for AI-powered style checking | No | `''` | +| `max-suggestions` | Maximum number of suggestions to make per file | No | `10` | +| `create-pr` | Whether to create a PR for scheduled mode (`true`/`false`) | No | `true` | + +## Outputs + +| Output | Description | +|--------|-------------| +| `files-reviewed` | Number of files reviewed | +| `suggestions-made` | Total number of suggestions made | +| `high-confidence-changes` | Number of high confidence changes auto-committed | +| `pr-url` | URL of created PR (in scheduled mode) | +| `review-summary` | Summary of the style review | + +## Configuration Examples + +### Custom Style Guide Location + +```yaml +- name: Style check with custom guide + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + style-guide: 'https://raw.githubusercontent.com/MyOrg/docs/main/style-guide.md' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### Exclude Specific Files + +```yaml +- name: Style check with exclusions + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + exclude-files: 'lectures/tmp/.*,.*_old\.md,lectures/archive/.*' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### Rule-Based Mode (No OpenAI) + +```yaml +- name: Rule-based style check + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + # No openai-api-key provided - uses rule-based fallback +``` + +## How It Works + +### 1. File Discovery +- **PR Mode**: Analyzes only files changed in the current pull request +- **Full Mode**: Analyzes all `.md` files in the specified `docs` directory +- Applies exclusion patterns to filter out unwanted files + +### 2. Style Analysis +- **AI-Powered (Recommended)**: Uses OpenAI GPT-4 with the style guide as context to provide intelligent, contextual suggestions +- **Rule-Based Fallback**: Uses predefined rules for common style issues when OpenAI is not available + +### 3. Confidence-Based Actions +- **PR Mode**: All suggestions are provided as GitHub review comments for transparency and reviewer control + - **High Confidence on Changed Lines**: Uses GitHub's suggestion feature for one-click application (requires line to be part of the PR diff) + - **High Confidence on Unchanged Lines**: Regular comments with suggested changes + - **Medium/Low Confidence**: Regular review comments with suggested changes +- **Full Mode**: + - **High Confidence**: Changes are auto-committed to maintain consistency + - **Medium/Low Confidence**: Suggested as review comments if in PR context + +### 4. GitHub Suggestions Integration +The action intelligently determines which suggestions can use GitHub's native suggestion feature: + +- **Diff-Based Suggestions**: For lines that are part of the PR diff, high-confidence suggestions use the `````suggestion` format that can be applied with one click +- **Non-Diff Suggestions**: For lines not changed in the PR, suggestions are provided as regular comments with the recommended changes +- **Transparent Process**: All suggestions require explicit reviewer approval - no automatic commits in PR mode + +### 4. Reporting +- Creates detailed PR comments with summaries and metrics +- Provides GitHub Action outputs for integration with other workflows + +## Style Rules Checked + +The action checks for compliance with the [QuantEcon Style Guide](../../copilot-qe-style-guide.md), including: + +### Writing Conventions +- Clarity and conciseness +- One sentence paragraphs +- Proper logical flow +- Appropriate capitalization + +### Code Style +- PEP8 compliance +- Unicode symbols for Greek letters (ฮฑ, ฮฒ, ฮณ, etc.) +- Modern timing patterns (`qe.Timer()`) +- Proper package installation practices + +### Math Notation +- LaTeX formatting standards +- Equation numbering and referencing +- Matrix and vector notation + +### Figure Formatting +- Caption formatting and placement +- Figure referencing +- Matplotlib styling guidelines + +### Document Structure +- Heading capitalization rules +- Proper linking syntax +- Citation formatting + +## Examples + +### Example PR Comment + +When the action runs on a pull request, it creates a summary comment like: + +```markdown +## ๐Ÿ“ Style Guide Review Summary + +**Files reviewed:** 3 +**Total suggestions:** 8 + +**Suggestions provided:** +- ๐Ÿ”ฅ **2** high-confidence suggestions (GitHub suggestions - click to apply) +- โš ๏ธ **4** medium-confidence suggestions +- ๐Ÿ’ก **2** low-confidence suggestions + +**Suggestion breakdown:** +- **High confidence**: Ready-to-apply suggestions using GitHub's suggestion feature +- **Medium confidence**: Recommended changes that may need minor adjustments +- **Low confidence**: Optional improvements for consideration + +All suggestions are based on the QuantEcon Style Guide. High-confidence suggestions can be applied with a single click using GitHub's suggestion feature for transparency and reviewer control. +``` + +### Example High-Confidence Suggestion + +High-confidence suggestions use GitHub's suggestion feature for easy application: + +```markdown +**Style Guide Suggestion (high confidence)** + +Use Unicode ฮฑ instead of 'alpha' for better mathematical notation + +**Rule category:** variable_naming + +```suggestion +def utility_function(c, ฮฑ=0.5, ฮฒ=0.95): +``` +``` + +### Example Medium/Low-Confidence Suggestion + +Other suggestions appear as regular review comments: + +```markdown +**Style Guide Suggestion (medium confidence)** + +Consider improving paragraph clarity by breaking into shorter sentences + +**Suggested change:** +```markdown +This sentence is too long and contains multiple ideas. It should be split for better readability. +``` + +**Rule category:** writing_conventions +``` + +## Setup Requirements + +### 1. GitHub Token +The action requires a GitHub token with appropriate permissions: +- For public repositories: `${{ secrets.GITHUB_TOKEN }}` (automatic) +- For private repositories: Personal access token with `repo` scope + +### 2. OpenAI API Key (Recommended) +For AI-powered analysis, add your OpenAI API key as a repository secret: +1. Go to repository Settings โ†’ Secrets and variables โ†’ Actions +2. Add a new secret named `OPENAI_API_KEY` +3. Paste your OpenAI API key as the value + +Without OpenAI, the action falls back to rule-based checking with limited capabilities. + +## Troubleshooting + +### Common Issues + +**"No files to review"** +- Check that the `docs` path exists and contains `.md` files +- Verify exclusion patterns aren't too broad +- In PR mode, ensure there are actual changes to markdown files + +**"OpenAI API not available"** +- Verify the `OPENAI_API_KEY` secret is correctly set +- Check API key has sufficient credits/quota +- Action will fall back to rule-based checking + +**"Failed to create PR review suggestions"** +- Ensure the GitHub token has appropriate permissions +- Check that the action is running in a PR context for PR mode + +### Debug Mode + +Add debug logging by setting the `ACTIONS_STEP_DEBUG` secret to `true` in your repository settings. + +## Contributing + +To contribute to this action: + +1. Modify the Python script in `check-style.py` +2. Update tests in the `/test/style-guide-checker/` directory +3. Run tests locally before submitting PR +4. Update documentation as needed + +## License + +This action is part of the QuantEcon meta repository and follows the same license terms. \ No newline at end of file diff --git a/.github/actions/style-guide-checker/action.yml b/.github/actions/style-guide-checker/action.yml new file mode 100644 index 0000000..a852a3f --- /dev/null +++ b/.github/actions/style-guide-checker/action.yml @@ -0,0 +1,99 @@ +name: 'Style Guide Checker' +description: 'AI-powered style guide compliance checker for QuantEcon lectures using MyST Markdown' +author: 'QuantEcon' + +inputs: + style-guide: + description: 'Path to the style guide document (local path or URL)' + required: false + default: '.github/copilot-qe-style-guide.md' + docs: + description: 'Path to the documents/lectures directory' + required: false + default: 'lectures' + exclude-files: + description: 'Comma-separated list of file patterns to exclude (supports regex)' + required: false + default: '' + mode: + description: 'Operating mode: "pr" for PR review, "full" for complete review' + required: false + default: 'pr' + confidence-threshold: + description: 'Minimum confidence level for auto-commits in full mode (high, medium, low). In PR mode, all suggestions use GitHub review comments.' + required: false + default: 'high' + github-token: + description: 'GitHub token for API access' + required: true + openai-api-key: + description: 'OpenAI API key for AI-powered style checking' + required: false + default: '' + max-suggestions: + description: 'Maximum number of suggestions to make per file' + required: false + default: '10' + create-pr: + description: 'Whether to create a PR for scheduled mode (true/false)' + required: false + default: 'true' + +outputs: + files-reviewed: + description: 'Number of files reviewed' + value: ${{ steps.check.outputs.files-reviewed }} + suggestions-made: + description: 'Total number of suggestions made' + value: ${{ steps.check.outputs.suggestions-made }} + high-confidence-changes: + description: 'Number of high confidence changes auto-committed (only in full mode)' + value: ${{ steps.check.outputs.high-confidence-changes }} + pr-url: + description: 'URL of created PR (in scheduled mode)' + value: ${{ steps.check.outputs.pr-url }} + review-summary: + description: 'Summary of the style review' + value: ${{ steps.check.outputs.review-summary }} + +runs: + using: 'composite' + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + shell: bash + run: | + pip install requests openai PyGithub gitpython + + - name: Run style guide checker + id: check + shell: bash + run: | + python3 ${{ github.action_path }}/check_style.py + env: + INPUT_STYLE_GUIDE: ${{ inputs.style-guide }} + INPUT_DOCS: ${{ inputs.docs }} + INPUT_EXCLUDE_FILES: ${{ inputs.exclude-files }} + INPUT_MODE: ${{ inputs.mode }} + INPUT_CONFIDENCE_THRESHOLD: ${{ inputs.confidence-threshold }} + INPUT_GITHUB_TOKEN: ${{ inputs.github-token }} + INPUT_OPENAI_API_KEY: ${{ inputs.openai-api-key }} + INPUT_MAX_SUGGESTIONS: ${{ inputs.max-suggestions }} + INPUT_CREATE_PR: ${{ inputs.create-pr }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_BASE_REF: ${{ github.base_ref }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF: ${{ github.ref }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_SERVER_URL: ${{ github.server_url }} + +branding: + icon: 'check-circle' + color: 'green' \ No newline at end of file diff --git a/.github/actions/style-guide-checker/check_style.py b/.github/actions/style-guide-checker/check_style.py new file mode 100644 index 0000000..147d2ed --- /dev/null +++ b/.github/actions/style-guide-checker/check_style.py @@ -0,0 +1,737 @@ +#!/usr/bin/env python3 +""" +Style Guide Checker for QuantEcon Lectures +========================================== + +AI-powered style guide compliance checker that reviews MyST Markdown files +for adherence to QuantEcon style guidelines. + +Supports two modes: +1. PR mode: Reviews only changed files in a pull request, provides all suggestions as GitHub review comments +2. Full mode: Reviews all files in the specified directory, auto-commits high-confidence changes + +Features: +- AI-powered style analysis using OpenAI API +- Confidence-based categorization of suggestions +- In PR mode: GitHub suggestions for high-confidence changes, review comments for others +- In Full mode: Automatic commits for high-confidence changes +- PR review suggestions for medium/low confidence changes +- Regex-based file exclusion +""" + +import os +import sys +import re +import json +import subprocess +import logging +from pathlib import Path +from typing import List, Dict, Optional, Tuple +from dataclasses import dataclass +from enum import Enum + +# Import required libraries +try: + import requests + REQUESTS_AVAILABLE = True +except ImportError: + REQUESTS_AVAILABLE = False + +try: + from github import Github + GITHUB_AVAILABLE = True +except ImportError: + GITHUB_AVAILABLE = False + +try: + import git + GIT_AVAILABLE = True +except ImportError: + GIT_AVAILABLE = False + +# Try to import OpenAI (optional) +try: + import openai + OPENAI_AVAILABLE = True +except ImportError: + OPENAI_AVAILABLE = False + + +class ConfidenceLevel(Enum): + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +@dataclass +class StyleSuggestion: + """Represents a style suggestion for a file""" + file_path: str + line_number: int + original_text: str + suggested_text: str + explanation: str + confidence: ConfidenceLevel + rule_category: str + + +class StyleGuideChecker: + """Main class for style guide checking functionality""" + + def __init__(self): + self.setup_logging() + self.load_environment() + self.setup_github() + self.setup_openai() + self.load_style_guide() + + def setup_logging(self): + """Configure logging""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def load_environment(self): + """Load environment variables and inputs""" + self.style_guide_path = os.getenv('INPUT_STYLE_GUIDE', '.github/copilot-qe-style-guide.md') + self.docs_path = os.getenv('INPUT_DOCS', 'lectures') + self.exclude_files = os.getenv('INPUT_EXCLUDE_FILES', '') + self.mode = os.getenv('INPUT_MODE', 'pr') + self.confidence_threshold = ConfidenceLevel(os.getenv('INPUT_CONFIDENCE_THRESHOLD', 'high')) + self.max_suggestions = int(os.getenv('INPUT_MAX_SUGGESTIONS', '10')) + self.create_pr = os.getenv('INPUT_CREATE_PR', 'true').lower() == 'true' + + # GitHub environment + self.github_token = os.getenv('INPUT_GITHUB_TOKEN') + self.openai_api_key = os.getenv('INPUT_OPENAI_API_KEY') + self.github_repository = os.getenv('GITHUB_REPOSITORY') + self.github_event_name = os.getenv('GITHUB_EVENT_NAME') + self.github_head_ref = os.getenv('GITHUB_HEAD_REF') + self.github_base_ref = os.getenv('GITHUB_BASE_REF') + self.github_sha = os.getenv('GITHUB_SHA') + self.github_ref = os.getenv('GITHUB_REF') + self.github_actor = os.getenv('GITHUB_ACTOR') + + if not self.github_token: + raise ValueError("GitHub token is required") + + def setup_github(self): + """Setup GitHub API client""" + if not GITHUB_AVAILABLE: + self.logger.warning("GitHub library not available - some features will be limited") + self.github_client = None + self.repo = None + return + + try: + self.github_client = Github(self.github_token) + self.repo = self.github_client.get_repo(self.github_repository) + self.logger.info(f"Connected to GitHub repository: {self.github_repository}") + except Exception as e: + self.logger.error(f"Failed to setup GitHub client: {e}") + self.github_client = None + self.repo = None + + def setup_openai(self): + """Setup OpenAI API client if available""" + if self.openai_api_key and OPENAI_AVAILABLE: + openai.api_key = self.openai_api_key + self.openai_enabled = True + self.logger.info("OpenAI API configured") + else: + self.openai_enabled = False + self.logger.warning("OpenAI API not available - using rule-based checking only") + + def load_style_guide(self): + """Load the style guide content""" + try: + # Check if it's a URL or local path + if self.style_guide_path.startswith('http'): + if not REQUESTS_AVAILABLE: + raise ImportError("requests library not available for URL loading") + response = requests.get(self.style_guide_path) + response.raise_for_status() + self.style_guide_content = response.text + else: + # Local path + style_guide_file = Path(self.style_guide_path) + if not style_guide_file.exists(): + raise FileNotFoundError(f"Style guide not found: {self.style_guide_path}") + self.style_guide_content = style_guide_file.read_text() + + self.logger.info(f"Loaded style guide from: {self.style_guide_path}") + + except Exception as e: + self.logger.error(f"Failed to load style guide: {e}") + # Use a minimal default style guide for testing + self.style_guide_content = "Use Unicode Greek letters (ฮฑ, ฮฒ, ฮณ) instead of spelled out names." + self.logger.warning("Using minimal default style guide") + + def get_files_to_review(self) -> List[str]: + """Get list of files to review based on mode""" + if self.mode == 'pr': + return self.get_changed_files() + else: + return self.get_all_markdown_files() + + def get_changed_files(self) -> List[str]: + """Get files changed in the current PR""" + try: + if self.github_event_name != 'pull_request' or not self.repo: + self.logger.warning("Not in PR context or GitHub not available, falling back to all files") + return self.get_all_markdown_files() + + # Get PR number from context + pr_number = self.get_pr_number() + if not pr_number: + return self.get_all_markdown_files() + + pr = self.repo.get_pull(pr_number) + changed_files = [] + + for file in pr.get_files(): + if file.filename.endswith('.md') and self.should_include_file(file.filename): + changed_files.append(file.filename) + + self.logger.info(f"Found {len(changed_files)} changed markdown files") + return changed_files + + except Exception as e: + self.logger.error(f"Failed to get changed files: {e}") + return self.get_all_markdown_files() + + def get_pr_number(self) -> Optional[int]: + """Extract PR number from GitHub context""" + try: + # Try to get from GITHUB_REF + if self.github_ref and 'pull' in self.github_ref: + match = re.search(r'refs/pull/(\d+)/merge', self.github_ref) + if match: + return int(match.group(1)) + + # Try to get from event file + event_path = os.getenv('GITHUB_EVENT_PATH') + if event_path and os.path.exists(event_path): + with open(event_path) as f: + event_data = json.load(f) + if 'pull_request' in event_data: + return event_data['pull_request']['number'] + + except Exception as e: + self.logger.warning(f"Could not determine PR number: {e}") + + return None + + def get_all_markdown_files(self) -> List[str]: + """Get all markdown files in the docs directory""" + docs_path = Path(self.docs_path) + if not docs_path.exists(): + self.logger.warning(f"Docs path does not exist: {self.docs_path}") + return [] + + markdown_files = [] + for md_file in docs_path.rglob('*.md'): + rel_path = str(md_file.relative_to('.')) + if self.should_include_file(rel_path): + markdown_files.append(rel_path) + + self.logger.info(f"Found {len(markdown_files)} markdown files") + return markdown_files + + def should_include_file(self, file_path: str) -> bool: + """Check if file should be included based on exclusion patterns""" + if not self.exclude_files: + return True + + exclude_patterns = [p.strip() for p in self.exclude_files.split(',')] + for pattern in exclude_patterns: + if pattern and re.search(pattern, file_path): + self.logger.debug(f"Excluding file: {file_path} (matches pattern: {pattern})") + return False + + return True + + def analyze_file_with_ai(self, file_path: str, content: str) -> List[StyleSuggestion]: + """Analyze file content using AI for style compliance""" + if not hasattr(self, 'openai_enabled') or not self.openai_enabled: + return self.analyze_file_with_rules(file_path, content) + + try: + prompt = self.build_ai_prompt(content) + + response = openai.ChatCompletion.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "You are an expert technical writer and educator specializing in QuantEcon style guidelines. Analyze the provided MyST Markdown content for compliance with the style guide and provide specific, actionable suggestions."}, + {"role": "user", "content": prompt} + ], + max_tokens=2000, + temperature=0.3 + ) + + return self.parse_ai_response(file_path, response.choices[0].message.content) + + except Exception as e: + self.logger.error(f"AI analysis failed for {file_path}: {e}") + return self.analyze_file_with_rules(file_path, content) + + def build_ai_prompt(self, content: str) -> str: + """Build prompt for AI analysis""" + return f""" +Please analyze the following MyST Markdown content for compliance with the QuantEcon style guide. + +STYLE GUIDE: +{self.style_guide_content} + +CONTENT TO ANALYZE: +{content} + +Please provide suggestions in the following JSON format: +{{ + "suggestions": [ + {{ + "line_number": , + "original_text": "", + "suggested_text": "", + "explanation": "", + "confidence": "", + "rule_category": "" + }} + ] +}} + +Focus on: +1. Writing conventions (clarity, conciseness, paragraph structure) +2. Code style (PEP8, variable naming, Unicode symbols) +3. Math notation (LaTeX formatting, equation numbering) +4. Figure formatting (captions, references, matplotlib settings) +5. Document structure (headings, linking, citations) + +Only suggest changes that clearly violate the style guide. Be conservative with suggestions. +Limit to {self.max_suggestions} suggestions maximum. +""" + + def parse_ai_response(self, file_path: str, response: str) -> List[StyleSuggestion]: + """Parse AI response into StyleSuggestion objects""" + try: + # Extract JSON from response + json_match = re.search(r'\{.*\}', response, re.DOTALL) + if not json_match: + self.logger.warning(f"No JSON found in AI response for {file_path}") + return [] + + data = json.loads(json_match.group()) + suggestions = [] + + for suggestion_data in data.get('suggestions', []): + suggestion = StyleSuggestion( + file_path=file_path, + line_number=suggestion_data.get('line_number', 1), + original_text=suggestion_data.get('original_text', ''), + suggested_text=suggestion_data.get('suggested_text', ''), + explanation=suggestion_data.get('explanation', ''), + confidence=ConfidenceLevel(suggestion_data.get('confidence', 'low')), + rule_category=suggestion_data.get('rule_category', 'general') + ) + suggestions.append(suggestion) + + return suggestions + + except Exception as e: + self.logger.error(f"Failed to parse AI response for {file_path}: {e}") + return [] + + def analyze_file_with_rules(self, file_path: str, content: str) -> List[StyleSuggestion]: + """Analyze file using rule-based approach as fallback""" + suggestions = [] + lines = content.split('\n') + + # Rule 1: Check for Greek letter usage in code contexts + greek_replacements = { + 'alpha': 'ฮฑ', 'beta': 'ฮฒ', 'gamma': 'ฮณ', 'delta': 'ฮด', + 'epsilon': 'ฮต', 'sigma': 'ฯƒ', 'theta': 'ฮธ', 'rho': 'ฯ' + } + + for i, line in enumerate(lines): + # Only check lines that contain code (function definitions, equations) + if ('def ' in line or '=' in line) and any(f'{english}' in line for english in greek_replacements.keys()): + for english, unicode_char in greek_replacements.items(): + # Look for the English word as a standalone parameter/variable + import re + pattern = r'\b' + english + r'\b' + if re.search(pattern, line): + new_line = re.sub(pattern, unicode_char, line) + if new_line != line: + suggestion = StyleSuggestion( + file_path=file_path, + line_number=i + 1, + original_text=line.strip(), + suggested_text=new_line.strip(), + explanation=f"Use Unicode {unicode_char} instead of '{english}' for better mathematical notation", + confidence=ConfidenceLevel.HIGH, + rule_category="variable_naming" + ) + suggestions.append(suggestion) + + # Rule 2: Check for capitalization in headings + for i, line in enumerate(lines): + if line.startswith('#') and not line.startswith('# '): + continue + if line.startswith('#'): + heading_text = line.lstrip('#').strip() + # Check if it's a lecture title (main heading) + if line.startswith('# '): + # Should be title case + words = heading_text.split() + title_case = ' '.join(word.capitalize() for word in words) + if heading_text != title_case: + suggestion = StyleSuggestion( + file_path=file_path, + line_number=i + 1, + original_text=line, + suggested_text=f"# {title_case}", + explanation="Lecture titles should use title case (capitalize all words)", + confidence=ConfidenceLevel.MEDIUM, + rule_category="titles_headings" + ) + suggestions.append(suggestion) + + # Rule 3: Check for manual timing patterns + for i, line in enumerate(lines): + if 'time.time()' in line and i < len(lines) - 3: + # Look for manual timing pattern in next few lines + following_lines = '\n'.join(lines[i:i+5]) + if 'start_time' in following_lines and 'end_time' in following_lines: + suggestion = StyleSuggestion( + file_path=file_path, + line_number=i + 1, + original_text=line, + suggested_text="with qe.Timer():", + explanation="Use modern qe.Timer() context manager instead of manual timing", + confidence=ConfidenceLevel.HIGH, + rule_category="performance_timing" + ) + suggestions.append(suggestion) + + return suggestions[:self.max_suggestions] + + def apply_high_confidence_changes(self, suggestions: List[StyleSuggestion]) -> int: + """Apply high confidence changes directly to files""" + changes_made = 0 + + # Group suggestions by file + file_suggestions = {} + for suggestion in suggestions: + if suggestion.confidence == ConfidenceLevel.HIGH: + if suggestion.file_path not in file_suggestions: + file_suggestions[suggestion.file_path] = [] + file_suggestions[suggestion.file_path].append(suggestion) + + for file_path, suggestions_list in file_suggestions.items(): + try: + # Read file content + with open(file_path, 'r') as f: + content = f.read() + + # Apply changes (in reverse line order to maintain line numbers) + lines = content.split('\n') + suggestions_list.sort(key=lambda x: x.line_number, reverse=True) + + for suggestion in suggestions_list: + if suggestion.line_number <= len(lines): + line_idx = suggestion.line_number - 1 + current_line = lines[line_idx] + # Only apply if the original text matches (trimmed for safety) + if current_line.strip() == suggestion.original_text or suggestion.original_text in current_line: + lines[line_idx] = suggestion.suggested_text + changes_made += 1 + self.logger.info(f"Applied change to {file_path}:{suggestion.line_number}") + else: + self.logger.warning(f"Skipping change to {file_path}:{suggestion.line_number} - line content doesn't match") + # Write back to file + with open(file_path, 'w') as f: + f.write('\n'.join(lines)) + + except Exception as e: + self.logger.error(f"Failed to apply changes to {file_path}: {e}") + + return changes_made + + def get_pr_diff_context(self, pr_number: int) -> Dict[str, Dict[int, int]]: + """Get mapping of file lines to diff positions for PR review suggestions""" + try: + pr = self.repo.get_pull(pr_number) + file_diff_map = {} + + for file in pr.get_files(): + if not file.filename.endswith('.md'): + continue + + file_diff_map[file.filename] = {} + + # Parse the patch to get line number to position mapping + if file.patch: + diff_position = 0 + for line in file.patch.split('\n'): + if line.startswith('@@'): + # Parse hunk header: @@ -old_start,old_count +new_start,new_count @@ + match = re.search(r'@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@', line) + if match: + current_line = int(match.group(1)) + elif line.startswith('+') and not line.startswith('+++'): + # Added line + file_diff_map[file.filename][current_line] = diff_position + current_line += 1 + elif line.startswith(' '): + # Context line + file_diff_map[file.filename][current_line] = diff_position + current_line += 1 + elif not line.startswith('-') and not line.startswith('\\'): + # Only increment for non-deleted lines + pass + + if not line.startswith('\\'): # Ignore "\ No newline at end of file" + diff_position += 1 + + return file_diff_map + + except Exception as e: + self.logger.error(f"Failed to get PR diff context: {e}") + return {} + + def create_pr_review_suggestions(self, suggestions: List[StyleSuggestion]): + """Create PR review suggestions for all confidence levels using GitHub suggestions""" + if self.github_event_name != 'pull_request' or not self.repo: + self.logger.info("Not in PR context or GitHub not available, skipping review suggestions") + return + + try: + pr_number = self.get_pr_number() + if not pr_number: + return + + pr = self.repo.get_pull(pr_number) + + # Get diff context to determine which lines can have review comments + diff_context = self.get_pr_diff_context(pr_number) + + # Separate suggestions that can use review comments vs regular comments + review_suggestions = [] + regular_suggestions = [] + + for suggestion in suggestions: + # Check if this line is in the diff and can have a review comment + if (suggestion.file_path in diff_context and + suggestion.line_number in diff_context[suggestion.file_path]): + review_suggestions.append(suggestion) + else: + regular_suggestions.append(suggestion) + + # Create review comments for suggestions on changed lines + if review_suggestions: + review_comments = [] + for suggestion in review_suggestions: + diff_position = diff_context[suggestion.file_path][suggestion.line_number] + + if suggestion.confidence == ConfidenceLevel.HIGH: + # Use GitHub suggestion format for high-confidence changes + comment_body = f"""**Style Guide Suggestion ({suggestion.confidence.value} confidence)** + +{suggestion.explanation} + +**Rule category:** {suggestion.rule_category} + +```suggestion +{suggestion.suggested_text} +```""" + else: + # Use regular comment format for medium/low confidence + comment_body = f"""**Style Guide Suggestion ({suggestion.confidence.value} confidence)** + +{suggestion.explanation} + +**Suggested change:** +```python +{suggestion.suggested_text} +``` + +**Rule category:** {suggestion.rule_category}""" + + review_comments.append({ + 'path': suggestion.file_path, + 'position': diff_position, + 'body': comment_body + }) + + if review_comments: + pr.create_review( + body="Style guide review completed. High-confidence suggestions are provided as GitHub suggestions that you can apply with one click.", + event="COMMENT", + comments=review_comments + ) + self.logger.info(f"Created {len(review_comments)} review suggestions on diff lines") + + # Create regular comments for suggestions not on changed lines + if regular_suggestions: + summary_body = "## Style Guide Suggestions\n\n" + summary_body += "The following suggestions are for lines not changed in this PR:\n\n" + + for suggestion in regular_suggestions: + summary_body += f"### `{suggestion.file_path}` (line {suggestion.line_number})\n\n" + summary_body += f"**{suggestion.explanation}**\n\n" + summary_body += f"**Rule category:** {suggestion.rule_category}\n\n" + summary_body += f"**Current:**\n```python\n{suggestion.original_text}\n```\n\n" + summary_body += f"**Suggested:**\n```python\n{suggestion.suggested_text}\n```\n\n" + summary_body += "---\n\n" + + pr.create_issue_comment(summary_body) + self.logger.info(f"Created summary comment for {len(regular_suggestions)} suggestions on non-diff lines") + + except Exception as e: + self.logger.error(f"Failed to create PR review suggestions: {e}") + + def create_pr_comment_summary(self, suggestions: List[StyleSuggestion], files_reviewed: int, changes_made: int = 0): + """Create a summary comment on the PR""" + if self.github_event_name != 'pull_request' or not self.repo: + self.logger.info("Not in PR context or GitHub not available, skipping PR comment") + return + + try: + pr_number = self.get_pr_number() + if not pr_number: + return + + pr = self.repo.get_pull(pr_number) + + high_conf = len([s for s in suggestions if s.confidence == ConfidenceLevel.HIGH]) + medium_conf = len([s for s in suggestions if s.confidence == ConfidenceLevel.MEDIUM]) + low_conf = len([s for s in suggestions if s.confidence == ConfidenceLevel.LOW]) + + summary = f""" +## ๐Ÿ“ Style Guide Review Summary + +**Files reviewed:** {files_reviewed} +**Total suggestions:** {len(suggestions)} + +**Suggestions provided:** +- ๐Ÿ”ฅ **{high_conf}** high-confidence suggestions (GitHub suggestions - click to apply) +- โš ๏ธ **{medium_conf}** medium-confidence suggestions +- ๐Ÿ’ก **{low_conf}** low-confidence suggestions + +**Suggestion breakdown:** +- **High confidence**: Ready-to-apply suggestions using GitHub's suggestion feature +- **Medium confidence**: Recommended changes that may need minor adjustments +- **Low confidence**: Optional improvements for consideration + +All suggestions are based on the [QuantEcon Style Guide]({self.style_guide_path}). High-confidence suggestions can be applied with a single click using GitHub's suggestion feature for transparency and reviewer control. + +--- +*Generated by [Style Guide Checker Action](https://github.com/QuantEcon/meta/.github/actions/style-guide-checker)* +""" + + pr.create_issue_comment(summary) + self.logger.info("Created PR summary comment") + + except Exception as e: + self.logger.error(f"Failed to create PR summary comment: {e}") + + def set_outputs(self, files_reviewed: int, suggestions_made: int, changes_made: int, pr_url: str = ""): + """Set GitHub Action outputs""" + github_output = os.getenv('GITHUB_OUTPUT') + if github_output: + with open(github_output, 'a') as f: + f.write(f"files-reviewed={files_reviewed}\n") + f.write(f"suggestions-made={suggestions_made}\n") + f.write(f"high-confidence-changes={changes_made}\n") + f.write(f"pr-url={pr_url}\n") + f.write(f"review-summary=Reviewed {files_reviewed} files, made {suggestions_made} suggestions, auto-applied {changes_made} high-confidence changes\n") + + def run(self): + """Main execution method""" + try: + self.logger.info(f"Starting style guide checker in {self.mode} mode") + + # Get files to review + files_to_review = self.get_files_to_review() + if not files_to_review: + self.logger.info("No files to review") + self.set_outputs(0, 0, 0) + return + + all_suggestions = [] + + # Analyze each file + for file_path in files_to_review: + try: + self.logger.info(f"Analyzing file: {file_path}") + + with open(file_path, 'r') as f: + content = f.read() + + suggestions = self.analyze_file_with_ai(file_path, content) + all_suggestions.extend(suggestions) + + self.logger.info(f"Found {len(suggestions)} suggestions for {file_path}") + + except Exception as e: + self.logger.error(f"Failed to analyze {file_path}: {e}") + + changes_made = 0 + + # Handle suggestions based on mode + if self.mode == 'pr': + # In PR mode, create review suggestions for all confidence levels + # High-confidence suggestions will use GitHub's suggestion feature + self.create_pr_review_suggestions(all_suggestions) + self.create_pr_comment_summary(all_suggestions, len(files_to_review)) + self.logger.info("PR mode: All suggestions provided as review comments with GitHub suggestions for high-confidence changes") + + else: + # In full mode, still apply high confidence changes and commit them + if self.confidence_threshold in [ConfidenceLevel.HIGH]: + changes_made = self.apply_high_confidence_changes(all_suggestions) + + # Commit changes if any were made + if changes_made > 0: + self.commit_changes(changes_made) + + self.logger.info(f"Full mode: Applied {changes_made} high-confidence changes") + + # Set outputs + self.set_outputs(len(files_to_review), len(all_suggestions), changes_made) + + self.logger.info(f"Style guide check completed: {len(files_to_review)} files, {len(all_suggestions)} suggestions, {changes_made} changes") + + except Exception as e: + self.logger.error(f"Style guide checker failed: {e}") + sys.exit(1) + + def commit_changes(self, changes_made: int): + """Commit any changes made by the style checker""" + try: + # Configure git + subprocess.run(['git', 'config', 'user.name', 'Style Guide Checker'], check=True) + subprocess.run(['git', 'config', 'user.email', 'style-checker@quantecon.org'], check=True) + + # Add and commit changes + subprocess.run(['git', 'add', '.'], check=True) + + commit_message = f"Apply {changes_made} high-confidence style guide suggestions\n\nAuto-applied by Style Guide Checker action" + subprocess.run(['git', 'commit', '-m', commit_message], check=True) + + self.logger.info(f"Committed {changes_made} style guide changes") + + except subprocess.CalledProcessError as e: + self.logger.error(f"Failed to commit changes: {e}") + + +def main(): + """Main entry point""" + checker = StyleGuideChecker() + checker.run() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/.github/actions/style-guide-checker/comment-trigger-workflow.yml b/.github/actions/style-guide-checker/comment-trigger-workflow.yml new file mode 100644 index 0000000..a8074b1 --- /dev/null +++ b/.github/actions/style-guide-checker/comment-trigger-workflow.yml @@ -0,0 +1,105 @@ +# Example workflow for comment-based style guide review +# This workflow triggers when someone comments with "/style-check" on a PR +name: "Style Guide Review (Comment Triggered)" + +"on": + issue_comment: + types: [created] + +jobs: + check-trigger: + # Only run on pull request comments (not issue comments) + if: github.event.issue.pull_request && (contains(github.event.comment.body, '/style-check') || contains(github.event.comment.body, '@quantecon-style-guide')) + runs-on: ubuntu-latest + name: Parse Comment Trigger + outputs: + should-run: ${{ steps.parse.outputs.should-run }} + trigger-phrase: ${{ steps.parse.outputs.trigger-phrase }} + steps: + - name: Parse comment for trigger phrases + id: parse + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + echo "Comment body: $COMMENT_BODY" + + # Check for trigger phrases + if echo "$COMMENT_BODY" | grep -q "/style-check"; then + echo "should-run=true" >> $GITHUB_OUTPUT + echo "trigger-phrase=/style-check" >> $GITHUB_OUTPUT + echo "โœ… Found /style-check trigger" + elif echo "$COMMENT_BODY" | grep -q "@quantecon-style-guide"; then + echo "should-run=true" >> $GITHUB_OUTPUT + echo "trigger-phrase=@quantecon-style-guide" >> $GITHUB_OUTPUT + echo "โœ… Found @quantecon-style-guide trigger" + else + echo "should-run=false" >> $GITHUB_OUTPUT + echo "โŒ No style check trigger found" + fi + + style-check: + needs: check-trigger + if: needs.check-trigger.outputs.should-run == 'true' + runs-on: ubuntu-latest + name: Style Guide Review + steps: + - name: Add reaction to comment + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + - name: Checkout PR + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + # Checkout the PR head, not the comment commit + ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.issue.number) }} + + - name: Run style guide checker + id: style-check + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' + confidence-threshold: 'high' + continue-on-error: true + + - name: Comment with results + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + ## ๐Ÿ” Style Guide Review Results + + **Triggered by:** ${{ needs.check-trigger.outputs.trigger-phrase }} + **Author:** @${{ github.event.comment.user.login }} + + **๐Ÿ“Š Summary:** + - **Files reviewed:** ${{ steps.style-check.outputs.files-reviewed }} + - **Suggestions made:** ${{ steps.style-check.outputs.suggestions-made }} + - **Auto-applied changes:** ${{ steps.style-check.outputs.high-confidence-changes }} + + ${{ steps.style-check.outputs.review-summary }} + + --- + ๐Ÿ’ก **Tip:** You can trigger this check anytime by commenting `/style-check` or `@quantecon-style-guide` + + - name: Add success reaction + if: success() + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '+1' + + - name: Add failure reaction + if: failure() + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '-1' \ No newline at end of file diff --git a/.github/actions/style-guide-checker/example-pr-workflow.yml b/.github/actions/style-guide-checker/example-pr-workflow.yml new file mode 100644 index 0000000..63d2734 --- /dev/null +++ b/.github/actions/style-guide-checker/example-pr-workflow.yml @@ -0,0 +1,27 @@ +# Example workflow for automatic PR style review +# This runs automatically when lecture files are changed +name: Style Guide Review (Automatic) + +on: + pull_request: + paths: + - 'lectures/**/*.md' + +jobs: + style-check: + runs-on: ubuntu-latest + name: Style Guide Review + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run style guide checker + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' + confidence-threshold: 'high' \ No newline at end of file diff --git a/.github/actions/style-guide-checker/example-scheduled-workflow.yml b/.github/actions/style-guide-checker/example-scheduled-workflow.yml new file mode 100644 index 0000000..799d41f --- /dev/null +++ b/.github/actions/style-guide-checker/example-scheduled-workflow.yml @@ -0,0 +1,28 @@ +# Example workflow for scheduled full repository review +name: Weekly Style Review + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 9 AM UTC + workflow_dispatch: # Allow manual triggering + +jobs: + full-style-review: + runs-on: ubuntu-latest + name: Full Repository Style Review + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run full style guide review + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'full' + create-pr: 'true' + confidence-threshold: 'high' + max-suggestions: 15 \ No newline at end of file diff --git a/.github/actions/style-guide-checker/examples.md b/.github/actions/style-guide-checker/examples.md new file mode 100644 index 0000000..bcf60bf --- /dev/null +++ b/.github/actions/style-guide-checker/examples.md @@ -0,0 +1,570 @@ +# Style Guide Checker Examples + +This document provides comprehensive examples of how to use the Style Guide Checker action in various scenarios. + +## Comment-Based Triggering (Recommended) + +### On-Demand PR Review via Comments + +The most user-friendly approach - users can trigger reviews by commenting on PRs: + +```yaml +name: Style Guide Review (Comment Triggered) +on: + issue_comment: + types: [created] + +jobs: + check-trigger: + if: github.event.issue.pull_request && (contains(github.event.comment.body, '/style-check') || contains(github.event.comment.body, '@quantecon-style-guide')) + runs-on: ubuntu-latest + name: Parse Comment Trigger + outputs: + should-run: ${{ steps.parse.outputs.should-run }} + trigger-phrase: ${{ steps.parse.outputs.trigger-phrase }} + steps: + - name: Parse comment for trigger phrases + id: parse + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + echo "Comment body: $COMMENT_BODY" + + if echo "$COMMENT_BODY" | grep -q "/style-check"; then + echo "should-run=true" >> $GITHUB_OUTPUT + echo "trigger-phrase=/style-check" >> $GITHUB_OUTPUT + elif echo "$COMMENT_BODY" | grep -q "@quantecon-style-guide"; then + echo "should-run=true" >> $GITHUB_OUTPUT + echo "trigger-phrase=@quantecon-style-guide" >> $GITHUB_OUTPUT + else + echo "should-run=false" >> $GITHUB_OUTPUT + fi + + style-check: + needs: check-trigger + if: needs.check-trigger.outputs.should-run == 'true' + runs-on: ubuntu-latest + name: Style Guide Review + steps: + - name: Add reaction to comment + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + - name: Checkout PR + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ format('refs/pull/{0}/head', github.event.issue.number) }} + + - name: Run style guide checker + id: style-check + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' + confidence-threshold: 'high' + + - name: Comment with results + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + ## ๐Ÿ” Style Guide Review Results + + **Triggered by:** ${{ needs.check-trigger.outputs.trigger-phrase }} + **Author:** @${{ github.event.comment.user.login }} + + **๐Ÿ“Š Summary:** + - **Files reviewed:** ${{ steps.style-check.outputs.files-reviewed }} + - **Suggestions made:** ${{ steps.style-check.outputs.suggestions-made }} + - **Auto-applied changes:** ${{ steps.style-check.outputs.high-confidence-changes }} + + ${{ steps.style-check.outputs.review-summary }} + + --- + ๐Ÿ’ก **Tip:** You can trigger this check anytime by commenting `/style-check` or `@quantecon-style-guide` + + - name: Add success reaction + if: success() + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '+1' +``` + +**How to use:** +1. Add this workflow to `.github/workflows/style-guide-comment.yml` +2. On any PR, comment `/style-check` or `@quantecon-style-guide` +3. The action will run and provide feedback directly in the PR + +## Basic Examples + +### 1. Simple PR Review + +The most basic usage for reviewing pull requests: + +```yaml +name: Style Review +on: + pull_request: + paths: + - '**/*.md' + +jobs: + style-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 2. Weekly Full Repository Review + +Scheduled review of all files with PR creation: + +```yaml +name: Weekly Style Audit +on: + schedule: + - cron: '0 9 * * 1' # Monday 9 AM UTC + +jobs: + full-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + mode: 'full' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + create-pr: 'true' +``` + +## Advanced Configuration Examples + +### 3. Custom Documentation Structure + +For repositories with non-standard directory structures: + +```yaml +- name: Style check custom structure + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + docs: 'content/lectures' + style-guide: 'docs/style/quantecon-guide.md' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 4. File Exclusion Patterns + +Excluding specific files and directories using regex patterns: + +```yaml +- name: Style check with exclusions + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + exclude-files: | + lectures/archive/.*, + .*_backup\.md, + lectures/tmp/.*, + lectures/old_version/.*, + README\.md + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 5. Conservative Mode (Medium Confidence Threshold) + +Only auto-commit very high confidence changes: + +```yaml +- name: Conservative style check + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + confidence-threshold: 'high' + max-suggestions: 5 + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 6. Aggressive Mode (Lower Confidence Threshold) + +Auto-commit more changes (use with caution): + +```yaml +- name: Aggressive style check + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + confidence-threshold: 'medium' + max-suggestions: 20 + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +## Organization-Wide Examples + +### 7. Shared Workflow Template + +Create a reusable workflow template in `.github/workflows/style-check.yml`: + +```yaml +name: Style Guide Compliance + +on: + pull_request: + paths: + - 'lectures/**/*.md' + - 'content/**/*.md' + workflow_dispatch: + inputs: + mode: + description: 'Review mode' + required: true + default: 'pr' + type: choice + options: + - pr + - full + +jobs: + style-review: + runs-on: ubuntu-latest + name: Style Guide Review + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run style guide checker + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + mode: ${{ github.event.inputs.mode || 'pr' }} + docs: ${{ vars.DOCS_PATH || 'lectures' }} + style-guide: ${{ vars.STYLE_GUIDE_PATH || '.github/copilot-qe-style-guide.md' }} + exclude-files: ${{ vars.EXCLUDE_PATTERNS || '' }} + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + max-suggestions: ${{ vars.MAX_SUGGESTIONS || '10' }} +``` + +### 8. Multi-Repository Style Enforcement + +For organization-wide style enforcement using repository variables: + +```yaml +name: Organization Style Standards + +on: + pull_request: + schedule: + - cron: '0 2 * * 0' # Weekly at 2 AM Sunday + +jobs: + determine-mode: + runs-on: ubuntu-latest + outputs: + mode: ${{ steps.mode.outputs.mode }} + steps: + - id: mode + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "mode=pr" >> $GITHUB_OUTPUT + else + echo "mode=full" >> $GITHUB_OUTPUT + fi + + style-check: + needs: determine-mode + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.ORGANIZATION_TOKEN }} + + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + mode: ${{ needs.determine-mode.outputs.mode }} + style-guide: 'https://raw.githubusercontent.com/QuantEcon/meta/main/.github/copilot-qe-style-guide.md' + docs: ${{ vars.LECTURES_PATH }} + exclude-files: ${{ vars.STYLE_EXCLUDE_PATTERNS }} + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +## Integration Examples + +### 9. Integration with Quality Checks + +Combine with other quality assurance actions: + +```yaml +name: Quality Assurance Pipeline + +on: + pull_request: + paths: + - 'lectures/**/*.md' + +jobs: + quality-checks: + runs-on: ubuntu-latest + strategy: + matrix: + check: [style, warnings, spelling] + steps: + - uses: actions/checkout@v4 + + - name: Style Guide Check + if: matrix.check == 'style' + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + + - name: Warning Check + if: matrix.check == 'warnings' + uses: QuantEcon/meta/.github/actions/check-warnings@main + with: + html-path: '_build/html' + + - name: Spelling Check + if: matrix.check == 'spelling' + uses: crate-ci/typos@master +``` + +### 10. Conditional Execution + +Run style checks only when certain conditions are met: + +```yaml +name: Conditional Style Check + +on: + pull_request: + types: [opened, synchronize, ready_for_review] + +jobs: + style-check: + if: | + !github.event.pull_request.draft && + contains(github.event.pull_request.labels.*.name, 'needs-style-review') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +## Fallback and Error Handling Examples + +### 11. Graceful Fallback + +Handle cases where OpenAI is unavailable: + +```yaml +- name: Style check with fallback + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + continue-on-error: true + +- name: Notify on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'โš ๏ธ Style guide check failed. Please review manually against the [style guide](/.github/copilot-qe-style-guide.md).' + }) +``` + +### 12. Rate Limiting Handling + +For large repositories, add delays to avoid API rate limits: + +```yaml +- name: Style check large repo + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + mode: 'full' + max-suggestions: 5 # Limit suggestions per file + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + timeout-minutes: 30 +``` + +## Testing and Development Examples + +### 13. Development Testing + +For testing the action during development: + +```yaml +name: Test Style Checker + +on: + push: + paths: + - '.github/actions/style-guide-checker/**' + workflow_dispatch: + inputs: + test-file: + description: 'Specific file to test' + required: false + +jobs: + test-action: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create test file + if: github.event.inputs.test-file + run: | + mkdir -p test-lectures + echo "${{ github.event.inputs.test-file }}" > test-lectures/test.md + + - name: Test style checker + uses: .//.github/actions/style-guide-checker + with: + docs: 'test-lectures' + mode: 'full' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 14. Style Guide Development + +Test with custom style guides during development: + +```yaml +- name: Test with custom style guide + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + style-guide: '.github/test-style-guide.md' + docs: 'test-content' + mode: 'full' + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +## Output Usage Examples + +### 15. Using Action Outputs + +Use outputs from the style checker in subsequent steps: + +```yaml +- name: Run style check + id: style + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + +- name: Report results + run: | + echo "Files reviewed: ${{ steps.style.outputs.files-reviewed }}" + echo "Suggestions made: ${{ steps.style.outputs.suggestions-made }}" + echo "Auto-applied changes: ${{ steps.style.outputs.high-confidence-changes }}" + +- name: Fail if too many issues + if: steps.style.outputs.suggestions-made > 50 + run: | + echo "Too many style issues found (${{ steps.style.outputs.suggestions-made }})" + exit 1 +``` + +### 16. Metrics Collection + +Collect style check metrics for analysis: + +```yaml +- name: Style check with metrics + id: style + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + +- name: Upload metrics + uses: actions/upload-artifact@v4 + with: + name: style-metrics + path: | + echo "date=$(date -u +%Y-%m-%d)" >> style-metrics.txt + echo "repository=${{ github.repository }}" >> style-metrics.txt + echo "files_reviewed=${{ steps.style.outputs.files-reviewed }}" >> style-metrics.txt + echo "suggestions_made=${{ steps.style.outputs.suggestions-made }}" >> style-metrics.txt + echo "auto_applied=${{ steps.style.outputs.high-confidence-changes }}" >> style-metrics.txt +``` + +## Security Examples + +### 17. Secure Token Handling + +Proper token management for organization use: + +```yaml +name: Secure Style Check + +jobs: + style-check: + runs-on: ubuntu-latest + environment: production # Use environment protection rules + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.STYLE_CHECK_TOKEN }} # Limited scope token + + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.STYLE_CHECK_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} +``` + +### 18. Fork Safety + +Safe execution for external contributions: + +```yaml +name: Safe Style Check + +on: + pull_request_target: # Use with caution + types: [labeled] + +jobs: + style-check: + if: contains(github.event.label.name, 'safe-to-test') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + # Note: No OpenAI key for external PRs - uses rule-based fallback +``` + +These examples cover most common use cases and can be adapted for specific repository needs. Remember to always test new configurations in a development environment before deploying to production workflows. \ No newline at end of file diff --git a/.github/workflows/test-style-guide-checker.yml b/.github/workflows/test-style-guide-checker.yml new file mode 100644 index 0000000..484e6c2 --- /dev/null +++ b/.github/workflows/test-style-guide-checker.yml @@ -0,0 +1,371 @@ +name: Test Style Guide Checker Action + +on: + push: + branches: [ main ] + paths: + - '.github/actions/style-guide-checker/**' + - 'test/style-guide-checker/**' + pull_request: + branches: [ main ] + paths: + - '.github/actions/style-guide-checker/**' + - 'test/style-guide-checker/**' + workflow_dispatch: + +jobs: + test-basic-functionality: + runs-on: ubuntu-latest + name: Test basic functionality + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install test dependencies + run: | + pip install pyyaml requests + + - name: Run basic tests + run: | + cd ${{ github.workspace }} + ./test/style-guide-checker/test-basic.sh + + test-clean-files: + runs-on: ubuntu-latest + name: Test with clean files (should find no issues) + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test action with clean files + id: clean-test + uses: .//.github/actions/style-guide-checker + with: + docs: 'test/style-guide-checker' + mode: 'full' + exclude-files: '.*bad.*\.md,.*exclude.*\.md' + github-token: ${{ secrets.GITHUB_TOKEN }} + # No OpenAI key - test rule-based fallback + continue-on-error: true + + - name: Verify clean results + run: | + echo "Files reviewed: ${{ steps.clean-test.outputs.files-reviewed }}" + echo "Suggestions made: ${{ steps.clean-test.outputs.suggestions-made }}" + echo "High confidence changes: ${{ steps.clean-test.outputs.high-confidence-changes }}" + + # Should find at least 1 file (clean-lecture.md) + if [ "${{ steps.clean-test.outputs.files-reviewed }}" -lt "1" ]; then + echo "โŒ Expected at least 1 file to be reviewed" + exit 1 + fi + + echo "โœ… Clean files test passed" + + test-files-with-issues: + runs-on: ubuntu-latest + name: Test with files containing style issues + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test action with files containing issues + id: issues-test + uses: .//.github/actions/style-guide-checker + with: + docs: 'test/style-guide-checker' + mode: 'full' + exclude-files: '.*clean.*\.md,.*exclude.*\.md' + max-suggestions: 20 + github-token: ${{ secrets.GITHUB_TOKEN }} + # No OpenAI key - test rule-based fallback + continue-on-error: true + + - name: Verify issues found + run: | + echo "Files reviewed: ${{ steps.issues-test.outputs.files-reviewed }}" + echo "Suggestions made: ${{ steps.issues-test.outputs.suggestions-made }}" + echo "High confidence changes: ${{ steps.issues-test.outputs.high-confidence-changes }}" + + # Should find at least 1 file (bad-style-lecture.md) + if [ "${{ steps.issues-test.outputs.files-reviewed }}" -lt "1" ]; then + echo "โŒ Expected at least 1 file to be reviewed" + exit 1 + fi + + # Rule-based checker should find some Greek letter issues + if [ "${{ steps.issues-test.outputs.suggestions-made }}" -lt "1" ]; then + echo "โŒ Expected at least some suggestions for files with style issues" + exit 1 + fi + + echo "โœ… Style issues test passed" + + test-file-exclusion: + runs-on: ubuntu-latest + name: Test file exclusion patterns + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test exclusion patterns + id: exclude-test + uses: .//.github/actions/style-guide-checker + with: + docs: 'test/style-guide-checker' + mode: 'full' + exclude-files: '.*exclude.*\.md' + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Verify exclusion worked + run: | + echo "Files reviewed: ${{ steps.exclude-test.outputs.files-reviewed }}" + + # Should review files but exclude exclude-me.md + # Should find at least 1 file but exclude the exclude-me.md file + if [ "${{ steps.exclude-test.outputs.files-reviewed }}" -lt "1" ]; then + echo "โŒ Expected at least 1 file to be reviewed after exclusion" + exit 1 + fi + + echo "โœ… File exclusion test passed" + + test-action-inputs: + runs-on: ubuntu-latest + name: Test various input configurations + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test with custom style guide (local) + uses: .//.github/actions/style-guide-checker + with: + style-guide: '.github/copilot-qe-style-guide.md' + docs: 'test/style-guide-checker' + mode: 'full' + confidence-threshold: 'medium' + max-suggestions: 5 + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Verify input configurations + run: | + echo "โœ… Action input configurations test passed" + + test-pr-mode-simulation: + runs-on: ubuntu-latest + name: Test PR mode simulation (fallback to full mode) + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test PR mode (should fallback to full) + id: pr-test + uses: .//.github/actions/style-guide-checker + with: + docs: 'test/style-guide-checker' + mode: 'pr' + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Verify PR mode handling + run: | + echo "Files reviewed: ${{ steps.pr-test.outputs.files-reviewed }}" + # In non-PR context, should fallback to full mode + echo "โœ… PR mode simulation test passed" + + test-yaml-validity: + runs-on: ubuntu-latest + name: Validate action YAML structure + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install PyYAML + run: pip install pyyaml + + - name: Validate action.yml + run: | + python3 -c " + import yaml + import sys + + with open('.github/actions/style-guide-checker/action.yml') as f: + try: + action_config = yaml.safe_load(f) + print('โœ… action.yml is valid YAML') + except yaml.YAMLError as e: + print(f'โŒ action.yml has YAML syntax error: {e}') + sys.exit(1) + + # Check required fields + required_fields = ['name', 'description', 'inputs', 'outputs', 'runs'] + for field in required_fields: + if field not in action_config: + print(f'โŒ Missing required field: {field}') + sys.exit(1) + + # Check that required inputs exist + required_inputs = ['github-token'] + for input_name in required_inputs: + if input_name not in action_config['inputs']: + print(f'โŒ Missing required input: {input_name}') + sys.exit(1) + + print('โœ… action.yml structure is valid') + " + + test-documentation: + runs-on: ubuntu-latest + name: Test documentation completeness + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check documentation files exist + run: | + required_docs=( + ".github/actions/style-guide-checker/README.md" + ".github/actions/style-guide-checker/action.yml" + ".github/actions/style-guide-checker/check_style.py" + ".github/actions/style-guide-checker/examples.md" + ) + + for doc in "${required_docs[@]}"; do + if [ ! -f "$doc" ]; then + echo "โŒ Missing documentation: $doc" + exit 1 + fi + echo "โœ… Found: $doc" + done + + echo "โœ… All documentation files present" + + - name: Check README completeness + run: | + readme=".github/actions/style-guide-checker/README.md" + + # Check for required sections + required_sections=( + "# Style Guide Checker Action" + "## Features" + "## Usage" + "## Inputs" + "## Outputs" + "## How It Works" + ) + + for section in "${required_sections[@]}"; do + if ! grep -q "$section" "$readme"; then + echo "โŒ Missing section in README: $section" + exit 1 + fi + echo "โœ… Found section: $section" + done + + echo "โœ… README documentation is complete" + + test-comment-trigger-workflow: + runs-on: ubuntu-latest + name: Test comment trigger workflow structure + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install PyYAML + run: pip install pyyaml + + - name: Validate comment trigger workflow + run: | + python3 -c " + import yaml + import sys + + workflow_file = '.github/actions/style-guide-checker/comment-trigger-workflow.yml' + + try: + with open(workflow_file) as f: + workflow = yaml.safe_load(f) + print('โœ… comment-trigger-workflow.yml is valid YAML') + except FileNotFoundError: + print(f'โŒ Missing comment trigger workflow: {workflow_file}') + sys.exit(1) + except yaml.YAMLError as e: + print(f'โŒ comment-trigger-workflow.yml has YAML syntax error: {e}') + sys.exit(1) + + # Check required workflow structure + if 'on' not in workflow: + print('โŒ Missing \"on\" trigger configuration') + sys.exit(1) + + if 'issue_comment' not in workflow['on']: + print('โŒ Missing issue_comment trigger') + sys.exit(1) + + if 'jobs' not in workflow: + print('โŒ Missing jobs configuration') + sys.exit(1) + + # Check for required jobs + required_jobs = ['check-trigger', 'style-check'] + for job in required_jobs: + if job not in workflow['jobs']: + print(f'โŒ Missing required job: {job}') + sys.exit(1) + + # Check comment parsing logic + check_trigger_job = workflow['jobs']['check-trigger'] + if 'if' not in check_trigger_job: + print('โŒ Missing conditional check for PR comments') + sys.exit(1) + + condition = check_trigger_job['if'] + if 'github.event.issue.pull_request' not in condition: + print('โŒ Missing PR validation in trigger condition') + sys.exit(1) + + if '/style-check' not in condition: + print('โŒ Missing /style-check trigger phrase') + sys.exit(1) + + if '@quantecon-style-guide' not in condition: + print('โŒ Missing @quantecon-style-guide trigger phrase') + sys.exit(1) + + print('โœ… Comment trigger workflow structure is valid') + print('โœ… Found required trigger phrases: /style-check and @quantecon-style-guide') + " + + - name: Test workflow files exist + run: | + required_files=( + ".github/actions/style-guide-checker/comment-trigger-workflow.yml" + ".github/actions/style-guide-checker/example-pr-workflow.yml" + ) + + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + echo "โŒ Missing workflow example: $file" + exit 1 + fi + echo "โœ… Found: $file" + done + + echo "โœ… All workflow files present" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38ccef2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Temporary files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Test outputs +/tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 8cef683..e2d72d8 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,24 @@ A GitHub Action that generates a weekly report summarizing issues and PR activit **Use case**: Automated weekly reporting on repository activity including opened/closed issues and merged PRs. Runs automatically every Saturday and creates an issue with the report. See the [action documentation](./.github/actions/weekly-report/README.md) for detailed usage instructions and examples. + +### Style Guide Checker Action + +A GitHub Action that uses AI to review QuantEcon lectures for compliance with the QuantEcon Style Guide and automatically suggests or applies improvements. + +**Location**: `.github/actions/style-guide-checker` + +**Usage**: +```yaml +- name: Style guide review + uses: QuantEcon/meta/.github/actions/style-guide-checker@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + docs: 'lectures' + mode: 'pr' +``` + +**Use case**: AI-powered style guide compliance checking for lectures with confidence-based automatic fixes and human review suggestions. Supports both PR mode (changed files only) and scheduled mode (full repository review). + +See the [action documentation](./.github/actions/style-guide-checker/README.md) for detailed usage instructions and examples. diff --git a/test/README.md b/test/README.md index bbeab05..95d745d 100644 --- a/test/README.md +++ b/test/README.md @@ -13,9 +13,16 @@ Each GitHub Action has its own test subdirectory: - `weekly-report/` - Tests for the `.github/actions/weekly-report` action - `test-basic.sh` - Basic functionality test for the weekly report action +- `style-guide-checker/` - Tests for the `.github/actions/style-guide-checker` action + - `clean-lecture.md` - Clean lecture file following style guide (negative test case) + - `bad-style-lecture.md` - Lecture with style issues (positive test case) + - `exclude-me.md` - File for testing exclusion patterns + - `test-basic.sh` - Basic functionality test for the style guide checker action + ## Running Tests Tests are automatically run by the GitHub Actions workflows in `.github/workflows/`. - For the `check-warnings` action, tests are run by the `test-warning-check.yml` workflow. -- For the `weekly-report` action, tests are run by the `test-weekly-report.yml` workflow. \ No newline at end of file +- For the `weekly-report` action, tests are run by the `test-weekly-report.yml` workflow. +- For the `style-guide-checker` action, tests are run by the `test-style-guide-checker.yml` workflow. \ No newline at end of file diff --git a/test/style-guide-checker/bad-style-lecture.md b/test/style-guide-checker/bad-style-lecture.md new file mode 100644 index 0000000..a9a7612 --- /dev/null +++ b/test/style-guide-checker/bad-style-lecture.md @@ -0,0 +1,55 @@ +# Test Lecture With Style Issues + +This lecture has multiple style guide violations that should be detected. + +## Bad code example with spelled-out Greek letters + +```python +import time +import numpy as np + +def utility_function(c, alpha=0.5, beta=0.95): + """Bad example using spelled-out Greek letters.""" + return (c**(1-alpha) - 1) / (1-alpha) * beta + +# Bad timing pattern +start_time = time.time() +result = utility_function(1.0) +end_time = time.time() +print(f"Execution time: {end_time - start_time:.4f} seconds") +``` + +## Heading Capitalization Issues + +### this heading should be capitalized properly + +This is another example of poor heading style. + +### Another Bad Example OF Capitalization + +This heading has inconsistent capitalization. + +## More Issues + +This section has various other style problems: + +- Using "alpha" instead of ฮฑ +- Manual timing instead of qe.Timer() +- Improper heading capitalization +- Missing proper math notation + +The variable gamma should be ฮณ, and delta should be ฮด. + +### Math Issues + +Bad equation formatting: + +$x_t = alpha * x_{t-1} + epsilon_t$ + +Should use proper Unicode and equation numbering. + +```python +# More bad Greek letter usage +def another_function(x, sigma=1.0, theta=0.5, rho=0.9): + return x * sigma + theta * rho +``` \ No newline at end of file diff --git a/test/style-guide-checker/clean-lecture.md b/test/style-guide-checker/clean-lecture.md new file mode 100644 index 0000000..a8dab11 --- /dev/null +++ b/test/style-guide-checker/clean-lecture.md @@ -0,0 +1,50 @@ +# Clean Test Lecture + +This is a test lecture that follows the QuantEcon style guide perfectly. + +## Introduction + +This lecture demonstrates proper style formatting. + +### Code example with proper Greek letters + +```python +import numpy as np +import quantecon as qe + +def utility_function(c, ฮฑ=0.5, ฮฒ=0.95): + """Utility function with proper Unicode Greek letters.""" + return (c**(1-ฮฑ) - 1) / (1-ฮฑ) * ฮฒ + +# Proper timing with qe.Timer() +with qe.Timer(): + result = utility_function(1.0) +``` + +### Math notation + +The equation is properly formatted: + +$$ +x_t = ฮฑ x_{t-1} + ฮต_t +$$ (eq:dynamics) + +We can reference it with {eq}`eq:dynamics`. + +### Figure example + +```{code-cell} ipython3 +--- +mystnb: + figure: + caption: sample data visualization + name: fig-sample +--- +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() +ax.plot([1, 2, 3], [1, 4, 2], lw=2) +ax.set_xlabel("time") +ax.set_ylabel("value") +plt.show() +``` \ No newline at end of file diff --git a/test/style-guide-checker/exclude-me.md b/test/style-guide-checker/exclude-me.md new file mode 100644 index 0000000..06ae264 --- /dev/null +++ b/test/style-guide-checker/exclude-me.md @@ -0,0 +1,11 @@ +# Test file for exclusion + +This file should be excluded from style checking when using the exclude pattern `.*exclude.*\.md`. + +It contains many style violations: +- alpha instead of ฮฑ +- beta instead of ฮฒ +- Bad timing patterns +- Improper headings + +But should not be checked when excluded. \ No newline at end of file diff --git a/test/style-guide-checker/test-basic.sh b/test/style-guide-checker/test-basic.sh new file mode 100755 index 0000000..48efddd --- /dev/null +++ b/test/style-guide-checker/test-basic.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# Basic test for the style guide checker action + +set -e + +echo "Testing style guide checker action..." + +# Mock environment variables for testing +export INPUT_STYLE_GUIDE=".github/copilot-qe-style-guide.md" +export INPUT_DOCS="test/style-guide-checker" +export INPUT_EXCLUDE_FILES="" +export INPUT_MODE="full" +export INPUT_CONFIDENCE_THRESHOLD="high" +export INPUT_GITHUB_TOKEN="fake-token-for-testing" +export INPUT_OPENAI_API_KEY="" # Test without OpenAI (rule-based fallback) +export INPUT_MAX_SUGGESTIONS="10" +export INPUT_CREATE_PR="false" +export GITHUB_OUTPUT="/tmp/github_output_test" +export GITHUB_REPOSITORY="QuantEcon/test-repo" +export GITHUB_EVENT_NAME="workflow_dispatch" + +# Create a temporary GitHub output file +echo "" > "$GITHUB_OUTPUT" + +# Test 1: Check that the Python script can be imported +echo "๐Ÿงช Test 1: Python script import test" +python3 -c " +import sys +sys.path.insert(0, '.github/actions/style-guide-checker') +import check_style +print('โœ… Python script imported successfully') +" + +# Test 2: Check rule-based analysis (without OpenAI) +echo "๐Ÿงช Test 2: Rule-based analysis test" +cd .github/actions/style-guide-checker +python3 -c " +import sys +import os +os.environ['INPUT_STYLE_GUIDE'] = '../../../.github/copilot-qe-style-guide.md' +os.environ['INPUT_DOCS'] = '../../../test/style-guide-checker' +os.environ['INPUT_MODE'] = 'full' +os.environ['INPUT_GITHUB_TOKEN'] = 'fake-token' +os.environ['INPUT_OPENAI_API_KEY'] = '' +os.environ['GITHUB_OUTPUT'] = '/tmp/test_output' +os.environ['GITHUB_REPOSITORY'] = 'test/repo' +os.environ['GITHUB_EVENT_NAME'] = 'workflow_dispatch' + +# Import and test directly +import check_style + +# Create instance and test basic file analysis +checker = check_style.StyleGuideChecker() +print('โœ… Rule-based analysis test passed') +" +cd ../../.. + +# Test 3: Test file exclusion patterns +echo "๐Ÿงช Test 3: File exclusion test" +python3 -c " +import re + +def should_include_file(file_path, exclude_files): + if not exclude_files: + return True + exclude_patterns = [p.strip() for p in exclude_files.split(',')] + for pattern in exclude_patterns: + if pattern and re.search(pattern, file_path): + return False + return True + +# Test exclusion patterns +test_files = [ + 'test/style-guide-checker/clean-lecture.md', + 'test/style-guide-checker/exclude-me.md', + 'test/style-guide-checker/bad-style-lecture.md' +] + +exclude_pattern = r'.*exclude.*\.md' +for file in test_files: + included = should_include_file(file, exclude_pattern) + if 'exclude' in file and included: + raise Exception(f'File {file} should have been excluded') + elif 'exclude' not in file and not included: + raise Exception(f'File {file} should have been included') + +print('โœ… File exclusion test passed') +" + +# Test 4: Check that action.yml is valid YAML +echo "๐Ÿงช Test 4: Action YAML validation" +python3 -c " +import yaml +with open('.github/actions/style-guide-checker/action.yml') as f: + action_config = yaml.safe_load(f) + +required_fields = ['name', 'description', 'inputs', 'outputs', 'runs'] +for field in required_fields: + if field not in action_config: + raise Exception(f'Missing required field: {field}') + +print('โœ… Action YAML validation passed') +" + +echo "โœ… All basic tests passed!" + +# Clean up +rm -f /tmp/github_output_test /tmp/test_output + +echo "๐ŸŽ‰ Style guide checker tests completed successfully!" \ No newline at end of file