diff --git a/.github/actions/code-style-checker/README.md b/.github/actions/code-style-checker/README.md new file mode 100644 index 0000000..c526a7a --- /dev/null +++ b/.github/actions/code-style-checker/README.md @@ -0,0 +1,360 @@ +# Code Style Formatter Action + +A GitHub Action that automatically formats Python code in MyST markdown files and standard markdown code blocks using [black](https://black.readthedocs.io/). + +## Features + +- **MyST Code-Cell Support**: Formats Python code in MyST `{code-cell}` directives +- **Standard Markdown Support**: Formats Python code in standard markdown fenced code blocks +- **Language Detection**: Automatically detects Python code by language identifiers (`python`, `python3`, `ipython`, `ipython3`) +- **Selective Processing**: Configure which types of code blocks to process +- **Individual Commits**: Creates separate commits for each modified file +- **Configurable Formatting**: Customize black formatting options +- **Comprehensive Outputs**: Detailed information about files processed and changes made + +## Usage + +### As a Standalone Action + +```yaml +- name: Format Python code in markdown files + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lecture/**/*.md' + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' +``` + +### With PR Comment Trigger + +The action automatically runs when a PR comment contains `@quantecon-code-style`: + +```yaml +name: Code Style Formatter +on: + issue_comment: + types: [created] + +jobs: + format-code: + if: github.event.issue.pull_request && contains(github.event.comment.body, '@quantecon-code-style') + runs-on: ubuntu-latest + + steps: + - name: Get PR information + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + core.setOutput('head-sha', pullRequest.head.sha); + core.setOutput('head-ref', pullRequest.head.ref); + core.setOutput('base-sha', pullRequest.base.sha); + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.pr.outputs.head-ref }} + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v40 + with: + files: '**/*.md' + base_sha: ${{ steps.pr.outputs.base-sha }} + sha: ${{ steps.pr.outputs.head-sha }} + + - name: Check if any markdown files changed + id: check-files + run: | + if [ -z "${{ steps.changed-files.outputs.all_changed_files }}" ]; then + echo "no-files=true" >> $GITHUB_OUTPUT + echo "No markdown files were changed in this PR" + else + echo "no-files=false" >> $GITHUB_OUTPUT + echo "Changed markdown files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + fi + + - name: Format MyST markdown files + if: steps.check-files.outputs.no-files == 'false' + id: format + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' + git-user-name: 'GitHub Action' + git-user-email: 'action@github.com' + + - name: Push changes + if: steps.check-files.outputs.no-files == 'false' && steps.format.outputs.changes-made == 'true' + run: | + git push + echo "Successfully pushed formatting changes" + + - name: Post comment with results + uses: actions/github-script@v7 + with: + script: | + const noFiles = '${{ steps.check-files.outputs.no-files }}'; + const changesMade = '${{ steps.format.outputs.changes-made }}'; + const filesProcessed = '${{ steps.format.outputs.files-processed }}'; + const filesChanged = '${{ steps.format.outputs.files-changed }}'; + const blocksFormatted = '${{ steps.format.outputs.total-blocks-formatted }}'; + + let body; + + if (noFiles === 'true') { + body = [ + '## šŸ” Code Style Check Results', + '', + 'āœ… **No markdown files were changed in this PR.**', + '', + 'The code style checker found no markdown files to process.', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else if (changesMade === 'true') { + body = [ + '## āœ… Code Style Formatting Applied', + '', + `šŸŽ‰ **Successfully applied black formatting to ${blocksFormatted} code block(s) across ${filesChanged} file(s).**`, + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + `- **Files modified:** ${filesChanged}`, + `- **Code blocks formatted:** ${blocksFormatted}`, + '', + '**Changes committed:**', + '- Each modified file has been committed separately with a descriptive commit message', + '- The formatting follows PEP8 standards using black', + '', + '**Languages processed:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else { + body = [ + '## āœ… Code Style Check Completed', + '', + `šŸ“ **Processed ${filesProcessed} markdown file(s) - no formatting changes needed.**`, + '', + 'All Python code blocks in the changed markdown files are already properly formatted according to PEP8 standards.', + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + '- **Files modified:** 0', + '- **Code blocks formatted:** 0', + '', + '**Languages checked:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); +``` + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `files` | Comma-separated list of markdown files to process or glob patterns (e.g., `lecture/**/*.md`) | āœ… | | +| `check-myst-code-cells` | Enable processing of MyST `{code-cell}` directives | āŒ | `true` | +| `check-markdown-blocks` | Enable processing of standard markdown fenced code blocks | āŒ | `true` | +| `python-languages` | Comma-separated list of language identifiers to treat as Python | āŒ | `python,python3,ipython,ipython3` | +| `black-args` | Additional arguments to pass to black | āŒ | `--line-length=88` | +| `commit-files` | Whether to commit changes to individual files | āŒ | `true` | +| `git-user-name` | Git user name for commits | āŒ | `GitHub Action` | +| `git-user-email` | Git user email for commits | āŒ | `action@github.com` | + +## Outputs + +| Output | Description | +|--------|-------------| +| `files-processed` | Number of files that were processed | +| `files-changed` | Number of files that had changes made | +| `total-blocks-formatted` | Total number of code blocks that were formatted | +| `changes-made` | Whether any changes were made to files (`true`/`false`) | + +## Code Block Types Supported + +### MyST Code-Cell Directives + +```markdown +```{code-cell} python +import numpy as np +def badly_formatted(x,y): + return x+y +``` +``` + +### Standard Markdown Fenced Blocks + +```markdown +```python +import numpy as np +def badly_formatted(x,y): + return x+y +``` +``` + +### Supported Language Identifiers + +By default, the action recognizes these language identifiers as Python code: +- `python` +- `python3` +- `ipython` +- `ipython3` + +You can customize this list using the `python-languages` input. + +## Example Workflow Trigger + +To trigger the formatter on a PR, simply comment: + +``` +@quantecon-code-style +``` + +The action will: +1. Find all changed markdown files in the PR +2. Extract Python code from MyST code-cells and markdown blocks +3. Apply black formatting to the code +4. Commit changes to individual files with descriptive messages +5. Post a summary comment with results + +## How It Works + +1. **File Detection**: Processes only `.md` files from the provided file list +2. **Code Extraction**: Uses regex patterns to find MyST `{code-cell}` directives and standard markdown fenced code blocks +3. **Language Filtering**: Only processes blocks with Python language identifiers +4. **Black Formatting**: Creates temporary Python files and runs black with specified arguments +5. **File Updates**: Replaces original code blocks with formatted versions +6. **Git Operations**: Commits each modified file individually with descriptive commit messages + +## File Input Formats + +The `files` input accepts multiple formats: + +### Explicit File Paths +Comma-separated list of specific file paths: +```yaml +files: 'lecture/aiyagari.md,lecture/mccall.md,examples/optimization.md' +``` + +### Glob Patterns +Use shell glob patterns to match multiple files: +```yaml +# All markdown files in lecture directory and subdirectories +files: 'lecture/**/*.md' + +# All markdown files in specific directories +files: 'lecture/*.md,examples/*.md' + +# Mixed patterns and explicit files +files: 'lecture/**/*.md,specific-file.md' +``` + +### Supported Glob Patterns +- `*` - matches any characters (excluding path separators) +- `**` - matches any characters including path separators (recursive) +- `?` - matches any single character +- `[abc]` - matches any character in the set +- `[a-z]` - matches any character in the range + +## Configuration Examples + +### Process All Files in Directory + +```yaml +- uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lecture/**/*.md' + check-myst-code-cells: 'true' + check-markdown-blocks: 'false' +``` + +### Only Process MyST Code-Cells + +```yaml +- uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lecture/**/*.md' + check-myst-code-cells: 'true' + check-markdown-blocks: 'false' +``` + +### Custom Black Configuration + +```yaml +- uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'docs/*.md' + black-args: '--line-length=100 --skip-string-normalization' +``` + +### Process Only Specific Python Variants + +```yaml +- uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'examples/*.md' + python-languages: 'python,ipython' +``` + +## Error Handling + +The action handles common issues gracefully: + +- **Invalid Python Code**: If black cannot format the code, the original code is preserved +- **Missing Files**: Non-existent files are skipped with a warning +- **Non-Python Languages**: Code blocks with other languages are skipped and logged +- **Empty Code Blocks**: Empty or whitespace-only blocks are ignored + +## Commit Messages + +When `commit-files` is enabled, each file gets its own commit with the format: + +``` +[filename.md] applying black changes to code +``` + +For example: +- `[aiyagari.md] applying black changes to code` +- `[mccall.md] applying black changes to code` + +## See Also + +- [Black Documentation](https://black.readthedocs.io/) +- [MyST Markdown](https://myst-parser.readthedocs.io/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) \ No newline at end of file diff --git a/.github/actions/code-style-checker/action.yml b/.github/actions/code-style-checker/action.yml new file mode 100644 index 0000000..db937bd --- /dev/null +++ b/.github/actions/code-style-checker/action.yml @@ -0,0 +1,386 @@ +name: 'Code Style Formatter' +description: 'Format Python code in MyST markdown files and standard markdown code blocks using black' +author: 'QuantEcon' + +inputs: + files: + description: 'Comma-separated list of markdown files to process or glob patterns (e.g., "lecture/**/*.md")' + required: true + check-myst-code-cells: + description: 'Enable searching for Python code in MyST code-cell directives' + required: false + default: 'true' + check-markdown-blocks: + description: 'Enable searching for Python code in standard markdown fenced code blocks' + required: false + default: 'true' + python-languages: + description: 'Comma-separated list of language identifiers to treat as Python code' + required: false + default: 'python,python3,ipython,ipython3' + black-args: + description: 'Additional arguments to pass to black' + required: false + default: '--line-length=88' + commit-files: + description: 'Whether to commit changes to individual files' + required: false + default: 'true' + git-user-name: + description: 'Git user name for commits' + required: false + default: 'GitHub Action' + git-user-email: + description: 'Git user email for commits' + required: false + default: 'action@github.com' + +outputs: + files-processed: + description: 'Number of files that were processed' + value: ${{ steps.format.outputs.files-processed }} + files-changed: + description: 'Number of files that had changes made' + value: ${{ steps.format.outputs.files-changed }} + total-blocks-formatted: + description: 'Total number of code blocks that were formatted' + value: ${{ steps.format.outputs.total-blocks-formatted }} + changes-made: + description: 'Whether any changes were made to files' + value: ${{ steps.format.outputs.changes-made }} + +runs: + using: 'composite' + steps: + - name: Setup Python environment + shell: bash + run: | + # Install black if not available + if ! python3 -c "import black" 2>/dev/null; then + echo "Installing black..." + python3 -m pip install black + else + echo "Black is already available" + fi + + - name: Format Python code in markdown files + id: format + shell: bash + run: | + # Parse inputs + FILES="${{ inputs.files }}" + CHECK_MYST="${{ inputs.check-myst-code-cells }}" + CHECK_MARKDOWN="${{ inputs.check-markdown-blocks }}" + PYTHON_LANGS="${{ inputs.python-languages }}" + BLACK_ARGS="${{ inputs.black-args }}" + COMMIT_FILES="${{ inputs.commit-files }}" + GIT_USER_NAME="${{ inputs.git-user-name }}" + GIT_USER_EMAIL="${{ inputs.git-user-email }}" + + echo "Processing files: $FILES" + echo "Check MyST code-cells: $CHECK_MYST" + echo "Check markdown blocks: $CHECK_MARKDOWN" + echo "Python languages: $PYTHON_LANGS" + echo "Black arguments: $BLACK_ARGS" + + # Convert comma-separated inputs to arrays and expand glob patterns + IFS=',' read -ra FILE_INPUT_ARRAY <<< "$FILES" + IFS=',' read -ra LANG_ARRAY <<< "$PYTHON_LANGS" + + # Expand glob patterns to actual file paths + FILE_ARRAY=() + for pattern in "${FILE_INPUT_ARRAY[@]}"; do + # Remove leading/trailing whitespace + pattern=$(echo "$pattern" | xargs) + + # Check if pattern contains glob characters + if [[ "$pattern" == *"*"* || "$pattern" == *"?"* || "$pattern" == *"["* ]]; then + echo "šŸ” Expanding glob pattern: $pattern" + # Use bash glob expansion with nullglob to handle no matches + shopt -s nullglob + expanded_files=($pattern) + shopt -u nullglob + + if [ ${#expanded_files[@]} -eq 0 ]; then + echo "āš ļø No files found matching pattern: $pattern" + else + echo " Found ${#expanded_files[@]} file(s) matching pattern" + for file in "${expanded_files[@]}"; do + # Only add markdown files + if [[ "$file" == *.md ]]; then + FILE_ARRAY+=("$file") + echo " + $file" + else + echo " - Skipping non-markdown file: $file" + fi + done + fi + else + # Direct file path (original behavior) + FILE_ARRAY+=("$pattern") + fi + done + + echo "šŸ“‚ Total files to process: ${#FILE_ARRAY[@]}" + + # Initialize counters + FILES_PROCESSED=0 + FILES_CHANGED=0 + TOTAL_BLOCKS_FORMATTED=0 + CHANGES_MADE="false" + + # Configure git if committing + if [ "$COMMIT_FILES" = "true" ]; then + git config --local user.email "$GIT_USER_EMAIL" + git config --local user.name "$GIT_USER_NAME" + fi + + # Create Python script for processing markdown files + cat > /tmp/format_markdown.py << 'EOF' +import re +import sys +import tempfile +import subprocess +import os +from pathlib import Path + +def is_python_language(lang, python_langs): + """Check if language identifier represents Python code""" + return lang.lower().strip() in [l.lower().strip() for l in python_langs] + +def format_code_with_black(code, black_args): + """Format Python code using black""" + try: + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(code) + temp_file = f.name + + # Run black on the temporary file + cmd = ['python3', '-m', 'black'] + black_args.split() + [temp_file] + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + # Read back the formatted code + with open(temp_file, 'r') as f: + formatted_code = f.read() + os.unlink(temp_file) + return formatted_code.rstrip() + '\n' if formatted_code.endswith('\n') else formatted_code + else: + print(f"Black formatting failed: {result.stderr}", file=sys.stderr) + os.unlink(temp_file) + return code + except Exception as e: + print(f"Error formatting code: {e}", file=sys.stderr) + return code + +def process_markdown_file(file_path, check_myst, check_markdown, python_langs, black_args): + """Process a markdown file and format Python code blocks""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + original_content = content + blocks_formatted = 0 + + # Process standard markdown fenced code blocks first if enabled + if check_markdown: + # Simple line-by-line approach that works reliably + lines = content.split('\n') + new_lines = [] + i = 0 + + while i < len(lines): + line = lines[i] + + # Skip MyST code-cell blocks entirely if we encounter them + if line.startswith('```{code-cell}'): + # Add the line and skip until we find the closing ``` + new_lines.append(line) + i += 1 + while i < len(lines) and not lines[i].startswith('```'): + new_lines.append(lines[i]) + i += 1 + # Add the closing ``` if found + if i < len(lines): + new_lines.append(lines[i]) + i += 1 + continue + + # Check if this line starts a standard code block + if line.startswith('```') and not line.startswith('```{code-cell}'): + # Extract language + lang = line[3:].strip() + + # Start collecting code block + code_lines = [] + new_lines.append(line) # Add the opening ``` + i += 1 + + # Collect lines until we find the closing ``` + while i < len(lines) and not lines[i].startswith('```'): + code_lines.append(lines[i]) + i += 1 + + # If we found a closing ```, process the code + if i < len(lines) and lines[i].startswith('```'): + # Check if this is Python code + if is_python_language(lang, python_langs): + code = '\n'.join(code_lines) + formatted_code = format_code_with_black(code, black_args) + blocks_formatted += 1 + print(f" Formatted markdown code block (language: {lang})") + + # Add the formatted code lines + for formatted_line in formatted_code.split('\n'): + if formatted_line or formatted_code.endswith('\n'): # Include last line if original had newline + new_lines.append(formatted_line) + else: + if lang: + print(f" Skipping markdown code block with language: {lang} (not Python)") + # Add the original code lines + new_lines.extend(code_lines) + + # Add the closing ``` + new_lines.append(lines[i]) + else: + # No closing ```, add the code lines as-is + new_lines.extend(code_lines) + else: + new_lines.append(line) + + i += 1 + + content = '\n'.join(new_lines) + + # Process MyST code-cell directives after standard blocks if enabled + if check_myst: + # Pattern for MyST code-cell directives: ```{code-cell} lang\ncode\n``` + pattern = r'```\{code-cell\}\s*([^\n]*)\n(.*?)\n```' + + def replace_myst_block(match): + nonlocal blocks_formatted + lang = match.group(1).strip() + code = match.group(2) + + if is_python_language(lang, python_langs): + formatted_code = format_code_with_black(code, black_args) + blocks_formatted += 1 + print(f" Formatted MyST code-cell block (language: {lang})") + return f'```{{code-cell}} {lang}\n{formatted_code}\n```' + else: + if lang: + print(f" Skipping MyST code-cell block with language: {lang} (not Python)") + return match.group(0) + + content = re.sub(pattern, replace_myst_block, content, flags=re.DOTALL) + + # Check if content changed + file_changed = content != original_content + + if file_changed: + # Write back the formatted content + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + return file_changed, blocks_formatted + + except Exception as e: + print(f"Error processing file {file_path}: {e}", file=sys.stderr) + return False, 0 + +if __name__ == "__main__": + if len(sys.argv) < 6: + print("Usage: python3 format_markdown.py ") + sys.exit(1) + + file_path = sys.argv[1] + check_myst = sys.argv[2].lower() == 'true' + check_markdown = sys.argv[3].lower() == 'true' + python_langs = sys.argv[4].split(',') + black_args = sys.argv[5] + + file_changed, blocks_formatted = process_markdown_file(file_path, check_myst, check_markdown, python_langs, black_args) + + # Output results + print(f"CHANGED:{file_changed}") + print(f"BLOCKS:{blocks_formatted}") +EOF + + # Process each file + for file in "${FILE_ARRAY[@]}"; do + # Skip empty entries + if [ -z "$file" ]; then + continue + fi + + # Remove leading/trailing whitespace (for explicit file paths) + file=$(echo "$file" | xargs) + + if [ ! -f "$file" ]; then + echo "āš ļø File not found: $file" + continue + fi + + if [[ ! "$file" == *.md ]]; then + echo "āš ļø Skipping non-markdown file: $file" + continue + fi + + echo "šŸ“ Processing file: $file" + FILES_PROCESSED=$((FILES_PROCESSED + 1)) + + # Run the Python script + output=$(python3 /tmp/format_markdown.py "$file" "$CHECK_MYST" "$CHECK_MARKDOWN" "$PYTHON_LANGS" "$BLACK_ARGS") + + # Parse output + file_changed=$(echo "$output" | grep "^CHANGED:" | cut -d: -f2) + blocks_formatted=$(echo "$output" | grep "^BLOCKS:" | cut -d: -f2) + + if [ "$file_changed" = "True" ]; then + FILES_CHANGED=$((FILES_CHANGED + 1)) + TOTAL_BLOCKS_FORMATTED=$((TOTAL_BLOCKS_FORMATTED + blocks_formatted)) + CHANGES_MADE="true" + + echo "āœ… Applied black formatting to $blocks_formatted code block(s) in $file" + + # Commit individual file if enabled + if [ "$COMMIT_FILES" = "true" ]; then + filename=$(basename "$file") + commit_msg="[${filename}] applying black changes to code" + + git add "$file" + if git diff --staged --quiet; then + echo "āš ļø No changes to commit for $file" + else + git commit -m "$commit_msg" + echo "šŸ“ Committed changes for $file with message: $commit_msg" + fi + fi + else + echo "āœ… No formatting changes needed for $file" + fi + done + + # Set outputs + echo "files-processed=$FILES_PROCESSED" >> $GITHUB_OUTPUT + echo "files-changed=$FILES_CHANGED" >> $GITHUB_OUTPUT + echo "total-blocks-formatted=$TOTAL_BLOCKS_FORMATTED" >> $GITHUB_OUTPUT + echo "changes-made=$CHANGES_MADE" >> $GITHUB_OUTPUT + + # Summary + echo "" + echo "šŸ“Š Summary:" + echo " Files processed: $FILES_PROCESSED" + echo " Files changed: $FILES_CHANGED" + echo " Total code blocks formatted: $TOTAL_BLOCKS_FORMATTED" + + if [ "$CHANGES_MADE" = "true" ]; then + echo "āœ… Code formatting completed with changes" + else + echo "āœ… Code formatting completed - no changes needed" + fi + +branding: + icon: 'code' + color: 'blue' \ No newline at end of file diff --git a/.github/actions/code-style-checker/examples.md b/.github/actions/code-style-checker/examples.md new file mode 100644 index 0000000..18c5283 --- /dev/null +++ b/.github/actions/code-style-checker/examples.md @@ -0,0 +1,530 @@ +# Code Style Formatter Action Examples + +This document provides practical examples of using the Code Style Formatter Action in different scenarios. + +## Table of Contents + +- [Basic Usage](#basic-usage) +- [Trigger on PR Comments](#trigger-on-pr-comments) +- [Selective Processing](#selective-processing) +- [Custom Black Configuration](#custom-black-configuration) +- [Integration with Existing Workflows](#integration-with-existing-workflows) +- [Multi-Repository Setup](#multi-repository-setup) + +## Basic Usage + +### Format Specific Files + +```yaml +name: Format Python Code +on: + push: + paths: + - 'lectures/**/*.md' + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Format Python code + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lectures/dynamic_programming.md,lectures/optimization.md' + + - name: Push changes + run: git push +``` + +### Format All Markdown Files Using Glob Patterns + +```yaml +- name: Format all lecture files + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lectures/**/*.md' + +- name: Format multiple directories + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lectures/**/*.md,examples/**/*.md,notebooks/**/*.md' +``` + +### Format All Markdown Files + +```yaml +- name: Get all markdown files using glob pattern + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: '**/*.md' + +- name: Format specific directories + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lectures/**/*.md,examples/**/*.md' +``` + +## Trigger on PR Comments + +### Complete PR Comment Workflow + +Copy this complete workflow to your repository as `.github/workflows/code-style-formatter.yml`: + +```yaml +name: Code Style Formatter +on: + issue_comment: + types: [created] + +jobs: + format-code: + if: github.event.issue.pull_request && contains(github.event.comment.body, '@quantecon-code-style') + runs-on: ubuntu-latest + + steps: + - name: Get PR information + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + core.setOutput('head-sha', pullRequest.head.sha); + core.setOutput('head-ref', pullRequest.head.ref); + core.setOutput('base-sha', pullRequest.base.sha); + + return pullRequest; + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.pr.outputs.head-ref }} + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v40 + with: + files: '**/*.md' + base_sha: ${{ steps.pr.outputs.base-sha }} + sha: ${{ steps.pr.outputs.head-sha }} + + - name: Check if any markdown files changed + id: check-files + run: | + if [ -z "${{ steps.changed-files.outputs.all_changed_files }}" ]; then + echo "no-files=true" >> $GITHUB_OUTPUT + echo "No markdown files were changed in this PR" + else + echo "no-files=false" >> $GITHUB_OUTPUT + echo "Changed markdown files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + fi + + - name: Format MyST markdown files + if: steps.check-files.outputs.no-files == 'false' + id: format + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' + git-user-name: 'GitHub Action' + git-user-email: 'action@github.com' + + - name: Push changes + if: steps.check-files.outputs.no-files == 'false' && steps.format.outputs.changes-made == 'true' + run: | + git push + echo "Successfully pushed formatting changes" + + - name: Post comment with results + uses: actions/github-script@v7 + with: + script: | + const noFiles = '${{ steps.check-files.outputs.no-files }}'; + const changesMade = '${{ steps.format.outputs.changes-made }}'; + const filesProcessed = '${{ steps.format.outputs.files-processed }}'; + const filesChanged = '${{ steps.format.outputs.files-changed }}'; + const blocksFormatted = '${{ steps.format.outputs.total-blocks-formatted }}'; + + let body; + + if (noFiles === 'true') { + body = [ + '## šŸ” Code Style Check Results', + '', + 'āœ… **No markdown files were changed in this PR.**', + '', + 'The code style checker found no markdown files to process.', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else if (changesMade === 'true') { + body = [ + '## āœ… Code Style Formatting Applied', + '', + `šŸŽ‰ **Successfully applied black formatting to ${blocksFormatted} code block(s) across ${filesChanged} file(s).**`, + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + `- **Files modified:** ${filesChanged}`, + `- **Code blocks formatted:** ${blocksFormatted}`, + '', + '**Changes committed:**', + '- Each modified file has been committed separately with a descriptive commit message', + '- The formatting follows PEP8 standards using black', + '', + '**Languages processed:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else { + body = [ + '## āœ… Code Style Check Completed', + '', + `šŸ“ **Processed ${filesProcessed} markdown file(s) - no formatting changes needed.**`, + '', + 'All Python code blocks in the changed markdown files are already properly formatted according to PEP8 standards.', + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + '- **Files modified:** 0', + '- **Code blocks formatted:** 0', + '', + '**Languages checked:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); +``` + +After adding this workflow to your repository, simply comment `@quantecon-code-style` on any PR to trigger automatic formatting of Python code in changed markdown files. + +### Custom Comment Triggers + +You can customize the trigger phrase: + +```yaml +if: github.event.issue.pull_request && contains(github.event.comment.body, '/format-python') +``` + +Or support multiple triggers: + +```yaml +if: | + github.event.issue.pull_request && ( + contains(github.event.comment.body, '@quantecon-code-style') || + contains(github.event.comment.body, '/format-code') || + contains(github.event.comment.body, '/black-format') + ) +``` + +## Selective Processing + +### MyST Code-Cells Only + +```yaml +- name: Format MyST code-cells only + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'true' + check-markdown-blocks: 'false' +``` + +### Standard Markdown Blocks Only + +```yaml +- name: Format markdown code blocks only + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'false' + check-markdown-blocks: 'true' +``` + +### Specific Python Variants + +```yaml +- name: Format only Python and IPython + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'notebooks/*.md' + python-languages: 'python,ipython' +``` + +## Custom Black Configuration + +### Line Length and Style Options + +```yaml +- name: Format with custom black settings + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: '--line-length=100 --skip-string-normalization' +``` + +### Target Python Version + +```yaml +- name: Format for Python 3.8+ + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: '--line-length=88 --target-version=py38' +``` + +### Skip Formatting Commits + +```yaml +- name: Format without committing + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + commit-files: 'false' + +- name: Custom commit message + if: steps.format.outputs.changes-made == 'true' + run: | + git add . + git commit -m "Apply black formatting to Python code blocks" + git push +``` + +## Integration with Existing Workflows + +### Pre-commit Integration + +```yaml +name: Pre-commit Checks +on: [push, pull_request] + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + + - name: Format Python in markdown + if: failure() # Only run if pre-commit fails + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} +``` + +### Build Process Integration + +```yaml +name: Build Documentation +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Format Python code + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lectures/*.md' + + - name: Build with Jupyter Book + run: jupyter-book build . + + - name: Deploy to GitHub Pages + if: github.ref == 'refs/heads/main' + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_build/html +``` + +## Multi-Repository Setup + +### Reusable Workflow + +Create `.github/workflows/format-python.yml`: + +```yaml +name: Reusable Python Formatter + +on: + workflow_call: + inputs: + file-pattern: + description: 'Pattern for files to format' + required: false + default: '**/*.md' + type: string + black-args: + description: 'Arguments for black' + required: false + default: '--line-length=88' + type: string + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v40 + with: + files: ${{ inputs.file-pattern }} + + - name: Format Python code + if: steps.changed-files.outputs.any_changed == 'true' + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: ${{ inputs.black-args }} + + - name: Push changes + if: steps.changed-files.outputs.any_changed == 'true' + run: git push +``` + +Use in other repositories: + +```yaml +name: Format Code +on: [push] + +jobs: + format: + uses: QuantEcon/meta/.github/workflows/format-python.yml@main + with: + file-pattern: 'lectures/**/*.md' + black-args: '--line-length=100' +``` + +### Organization-wide Settings + +For consistent formatting across all repositories: + +```yaml +- name: Format with organization standards + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: '--line-length=88 --target-version=py39' + python-languages: 'python,python3,ipython,ipython3' + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' +``` + +## Advanced Examples + +### Conditional Formatting Based on File Changes + +```yaml +- name: Get changed file types + id: changes + uses: dorny/paths-filter@v2 + with: + filters: | + lectures: + - 'lectures/**/*.md' + notebooks: + - 'notebooks/**/*.md' + examples: + - 'examples/**/*.md' + +- name: Format lecture files + if: steps.changes.outputs.lectures == 'true' + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: '--line-length=88' + +- name: Format notebook files + if: steps.changes.outputs.notebooks == 'true' + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + black-args: '--line-length=100' # Different settings for notebooks +``` + +### Error Handling and Notifications + +```yaml +- name: Format Python code + id: format + continue-on-error: true + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + +- name: Report formatting results + uses: actions/github-script@v7 + with: + script: | + const success = '${{ steps.format.outcome }}' === 'success'; + const changesMode = '${{ steps.format.outputs.changes-made }}' === 'true'; + + let message = success ? 'āœ… Code formatting completed' : 'āŒ Code formatting failed'; + if (success && changesMode) { + message += `\n\nšŸ“ Formatted ${{ steps.format.outputs.total-blocks-formatted }} code blocks in ${{ steps.format.outputs.files-changed }} files.`; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: message + }); +``` + +## Troubleshooting + +### Common Issues and Solutions + +1. **No files processed**: Ensure the file paths are correct and files exist +2. **Black formatting errors**: Check that the Python code is syntactically valid +3. **Permission issues**: Ensure the GitHub token has write permissions +4. **Large files**: Consider processing files in batches for very large repositories + +### Debug Mode + +```yaml +- name: Format with debug output + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + env: + ACTIONS_STEP_DEBUG: true +``` \ No newline at end of file diff --git a/.github/workflows/code-style-formatter-template.yml b/.github/workflows/code-style-formatter-template.yml new file mode 100644 index 0000000..dad2698 --- /dev/null +++ b/.github/workflows/code-style-formatter-template.yml @@ -0,0 +1,155 @@ +# Template workflow for repositories using the Code Style Checker Action +# Copy this file to your repository as .github/workflows/code-style-formatter.yml +# and it will trigger when someone comments '@quantecon-code-style' on a PR + +name: Code Style Formatter +on: + issue_comment: + types: [created] + +jobs: + format-code: + if: github.event.issue.pull_request && contains(github.event.comment.body, '@quantecon-code-style') + runs-on: ubuntu-latest + + steps: + - name: Get PR information + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + core.setOutput('head-sha', pullRequest.head.sha); + core.setOutput('head-ref', pullRequest.head.ref); + core.setOutput('base-sha', pullRequest.base.sha); + + return pullRequest; + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.pr.outputs.head-ref }} + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v40 + with: + files: '**/*.md' + base_sha: ${{ steps.pr.outputs.base-sha }} + sha: ${{ steps.pr.outputs.head-sha }} + + - name: Check if any markdown files changed + id: check-files + run: | + if [ -z "${{ steps.changed-files.outputs.all_changed_files }}" ]; then + echo "no-files=true" >> $GITHUB_OUTPUT + echo "No markdown files were changed in this PR" + else + echo "no-files=false" >> $GITHUB_OUTPUT + echo "Changed markdown files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + fi + + - name: Format MyST markdown files + if: steps.check-files.outputs.no-files == 'false' + id: format + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' + git-user-name: 'GitHub Action' + git-user-email: 'action@github.com' + + - name: Push changes + if: steps.check-files.outputs.no-files == 'false' && steps.format.outputs.changes-made == 'true' + run: | + git push + echo "Successfully pushed formatting changes" + + - name: Post comment with results + uses: actions/github-script@v7 + with: + script: | + const noFiles = '${{ steps.check-files.outputs.no-files }}'; + const changesMade = '${{ steps.format.outputs.changes-made }}'; + const filesProcessed = '${{ steps.format.outputs.files-processed }}'; + const filesChanged = '${{ steps.format.outputs.files-changed }}'; + const blocksFormatted = '${{ steps.format.outputs.total-blocks-formatted }}'; + + let body; + + if (noFiles === 'true') { + body = [ + '## šŸ” Code Style Check Results', + '', + 'āœ… **No markdown files were changed in this PR.**', + '', + 'The code style checker found no markdown files to process.', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else if (changesMade === 'true') { + body = [ + '## āœ… Code Style Formatting Applied', + '', + `šŸŽ‰ **Successfully applied black formatting to ${blocksFormatted} code block(s) across ${filesChanged} file(s).**`, + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + `- **Files modified:** ${filesChanged}`, + `- **Code blocks formatted:** ${blocksFormatted}`, + '', + '**Changes committed:**', + '- Each modified file has been committed separately with a descriptive commit message', + '- The formatting follows PEP8 standards using black', + '', + '**Languages processed:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else { + body = [ + '## āœ… Code Style Check Completed', + '', + `šŸ“ **Processed ${filesProcessed} markdown file(s) - no formatting changes needed.**`, + '', + 'All Python code blocks in the changed markdown files are already properly formatted according to PEP8 standards.', + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + '- **Files modified:** 0', + '- **Code blocks formatted:** 0', + '', + '**Languages checked:**', + '- \`python\`, \`python3\`, \`ipython\`, \`ipython3\` code blocks', + '- Both MyST \`{code-cell}\` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); \ No newline at end of file diff --git a/.github/workflows/code-style-formatter.yml b/.github/workflows/code-style-formatter.yml new file mode 100644 index 0000000..c11abb6 --- /dev/null +++ b/.github/workflows/code-style-formatter.yml @@ -0,0 +1,151 @@ +name: Code Style Formatter +on: + issue_comment: + types: [created] + +jobs: + format-code: + if: github.event.issue.pull_request && contains(github.event.comment.body, '@quantecon-code-style') + runs-on: ubuntu-latest + + steps: + - name: Get PR information + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + core.setOutput('head-sha', pullRequest.head.sha); + core.setOutput('head-ref', pullRequest.head.ref); + core.setOutput('base-sha', pullRequest.base.sha); + + return pullRequest; + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.pr.outputs.head-ref }} + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v40 + with: + files: '**/*.md' + base_sha: ${{ steps.pr.outputs.base-sha }} + sha: ${{ steps.pr.outputs.head-sha }} + + - name: Check if any markdown files changed + id: check-files + run: | + if [ -z "${{ steps.changed-files.outputs.all_changed_files }}" ]; then + echo "no-files=true" >> $GITHUB_OUTPUT + echo "No markdown files were changed in this PR" + else + echo "no-files=false" >> $GITHUB_OUTPUT + echo "Changed markdown files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + fi + + - name: Format MyST markdown files + if: steps.check-files.outputs.no-files == 'false' + id: format + uses: .//.github/actions/code-style-checker + with: + files: ${{ steps.changed-files.outputs.all_changed_files }} + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' + git-user-name: 'GitHub Action' + git-user-email: 'action@github.com' + + - name: Push changes + if: steps.check-files.outputs.no-files == 'false' && steps.format.outputs.changes-made == 'true' + run: | + git push + echo "Successfully pushed formatting changes" + + - name: Post comment with results + uses: actions/github-script@v7 + with: + script: | + const noFiles = '${{ steps.check-files.outputs.no-files }}'; + const changesMade = '${{ steps.format.outputs.changes-made }}'; + const filesProcessed = '${{ steps.format.outputs.files-processed }}'; + const filesChanged = '${{ steps.format.outputs.files-changed }}'; + const blocksFormatted = '${{ steps.format.outputs.total-blocks-formatted }}'; + + let body; + + if (noFiles === 'true') { + body = [ + '## šŸ” Code Style Check Results', + '', + 'āœ… **No markdown files were changed in this PR.**', + '', + 'The code style checker found no markdown files to process.', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else if (changesMade === 'true') { + body = [ + '## āœ… Code Style Formatting Applied', + '', + `šŸŽ‰ **Successfully applied black formatting to ${blocksFormatted} code block(s) across ${filesChanged} file(s).**`, + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + `- **Files modified:** ${filesChanged}`, + `- **Code blocks formatted:** ${blocksFormatted}`, + '', + '**Changes committed:**', + '- Each modified file has been committed separately with a descriptive commit message', + '- The formatting follows PEP8 standards using black', + '', + '**Languages processed:**', + '- `python`, `python3`, `ipython`, `ipython3` code blocks', + '- Both MyST `{code-cell}` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } else { + body = [ + '## āœ… Code Style Check Completed', + '', + `šŸ“ **Processed ${filesProcessed} markdown file(s) - no formatting changes needed.**`, + '', + 'All Python code blocks in the changed markdown files are already properly formatted according to PEP8 standards.', + '', + '**Summary:**', + `- **Files processed:** ${filesProcessed}`, + '- **Files modified:** 0', + '- **Code blocks formatted:** 0', + '', + '**Languages checked:**', + '- `python`, `python3`, `ipython`, `ipython3` code blocks', + '- Both MyST `{code-cell}` directives and standard markdown fenced code blocks', + '', + '---', + '', + 'šŸ¤– *This comment was automatically generated by the [Code Style Formatter](https://github.com/QuantEcon/meta/.github/actions/code-style-checker).*' + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); \ No newline at end of file diff --git a/.github/workflows/test-code-style-checker.yml b/.github/workflows/test-code-style-checker.yml new file mode 100644 index 0000000..b8cb371 --- /dev/null +++ b/.github/workflows/test-code-style-checker.yml @@ -0,0 +1,434 @@ +name: Test Code Style Checker Action + +on: + push: + branches: [ main ] + paths: + - '.github/actions/code-style-checker/**' + - 'test/code-style-checker/**' + pull_request: + branches: [ main ] + paths: + - '.github/actions/code-style-checker/**' + - 'test/code-style-checker/**' + workflow_dispatch: + +jobs: + test-unformatted-files: + runs-on: ubuntu-latest + name: Test with unformatted Python code + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create a copy of test files to work with + cp test/code-style-checker/unformatted-code.md /tmp/test-unformatted.md + + - name: Test action with unformatted code + id: unformatted-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-unformatted.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify unformatted results + run: | + echo "Files processed: ${{ steps.unformatted-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.unformatted-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.unformatted-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.unformatted-test.outputs.changes-made }}" + + if [ "${{ steps.unformatted-test.outputs.files-processed }}" != "1" ]; then + echo "āŒ Expected 1 file processed but got ${{ steps.unformatted-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.unformatted-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + if [ "${{ steps.unformatted-test.outputs.files-changed }}" != "1" ]; then + echo "āŒ Expected 1 file changed but got ${{ steps.unformatted-test.outputs.files-changed }}" + exit 1 + fi + + if [ "${{ steps.unformatted-test.outputs.total-blocks-formatted }}" -lt "4" ]; then + echo "āŒ Expected at least 4 blocks formatted but got ${{ steps.unformatted-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Unformatted code test passed" + + test-formatted-files: + runs-on: ubuntu-latest + name: Test with already formatted Python code + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create a copy of test files to work with + cp test/code-style-checker/formatted-code.md /tmp/test-formatted.md + + - name: Test action with formatted code + id: formatted-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-formatted.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify formatted results + run: | + echo "Files processed: ${{ steps.formatted-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.formatted-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.formatted-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.formatted-test.outputs.changes-made }}" + + if [ "${{ steps.formatted-test.outputs.files-processed }}" != "1" ]; then + echo "āŒ Expected 1 file processed but got ${{ steps.formatted-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.formatted-test.outputs.changes-made }}" != "false" ]; then + echo "āŒ Expected no changes to be made but changes were made" + exit 1 + fi + + if [ "${{ steps.formatted-test.outputs.files-changed }}" != "0" ]; then + echo "āŒ Expected 0 files changed but got ${{ steps.formatted-test.outputs.files-changed }}" + exit 1 + fi + + if [ "${{ steps.formatted-test.outputs.total-blocks-formatted }}" != "0" ]; then + echo "āŒ Expected 0 blocks formatted but got ${{ steps.formatted-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Formatted code test passed" + + test-no-python-files: + runs-on: ubuntu-latest + name: Test with non-Python code + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create a copy of test files to work with + cp test/code-style-checker/no-python-code.md /tmp/test-no-python.md + + - name: Test action with no Python code + id: no-python-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-no-python.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify no Python code results + run: | + echo "Files processed: ${{ steps.no-python-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.no-python-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.no-python-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.no-python-test.outputs.changes-made }}" + + if [ "${{ steps.no-python-test.outputs.files-processed }}" != "1" ]; then + echo "āŒ Expected 1 file processed but got ${{ steps.no-python-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.no-python-test.outputs.changes-made }}" != "false" ]; then + echo "āŒ Expected no changes to be made but changes were made" + exit 1 + fi + + if [ "${{ steps.no-python-test.outputs.files-changed }}" != "0" ]; then + echo "āŒ Expected 0 files changed but got ${{ steps.no-python-test.outputs.files-changed }}" + exit 1 + fi + + if [ "${{ steps.no-python-test.outputs.total-blocks-formatted }}" != "0" ]; then + echo "āŒ Expected 0 blocks formatted but got ${{ steps.no-python-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… No Python code test passed" + + test-myst-only: + runs-on: ubuntu-latest + name: Test MyST code-cell only processing + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create a copy of test files to work with + cp test/code-style-checker/unformatted-code.md /tmp/test-myst-only.md + + - name: Test action with MyST only + id: myst-only-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-myst-only.md' + check-myst-code-cells: 'true' + check-markdown-blocks: 'false' + commit-files: 'false' # Don't commit during tests + + - name: Verify MyST only results + run: | + echo "Files processed: ${{ steps.myst-only-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.myst-only-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.myst-only-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.myst-only-test.outputs.changes-made }}" + + if [ "${{ steps.myst-only-test.outputs.files-processed }}" != "1" ]; then + echo "āŒ Expected 1 file processed but got ${{ steps.myst-only-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.myst-only-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + # Should only format MyST blocks (3: python, python3, ipython3), not markdown blocks (2: python, ipython) + if [ "${{ steps.myst-only-test.outputs.total-blocks-formatted }}" != "3" ]; then + echo "āŒ Expected exactly 3 MyST blocks formatted but got ${{ steps.myst-only-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… MyST only test passed" + + test-markdown-only: + runs-on: ubuntu-latest + name: Test markdown blocks only processing + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create a copy of test files to work with + cp test/code-style-checker/unformatted-code.md /tmp/test-markdown-only.md + + - name: Test action with markdown blocks only + id: markdown-only-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-markdown-only.md' + check-myst-code-cells: 'false' + check-markdown-blocks: 'true' + commit-files: 'false' # Don't commit during tests + + - name: Verify markdown only results + run: | + echo "Files processed: ${{ steps.markdown-only-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.markdown-only-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.markdown-only-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.markdown-only-test.outputs.changes-made }}" + + if [ "${{ steps.markdown-only-test.outputs.files-processed }}" != "1" ]; then + echo "āŒ Expected 1 file processed but got ${{ steps.markdown-only-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.markdown-only-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + # Should only format markdown blocks (2: python, ipython), not MyST blocks (3: python, python3, ipython3) + if [ "${{ steps.markdown-only-test.outputs.total-blocks-formatted }}" != "2" ]; then + echo "āŒ Expected exactly 2 markdown blocks formatted but got ${{ steps.markdown-only-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Markdown only test passed" + + test-multiple-files: + runs-on: ubuntu-latest + name: Test processing multiple files + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create copies of test files to work with + cp test/code-style-checker/unformatted-code.md /tmp/test-multi-1.md + cp test/code-style-checker/formatted-code.md /tmp/test-multi-2.md + cp test/code-style-checker/no-python-code.md /tmp/test-multi-3.md + + - name: Test action with multiple files + id: multi-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/test-multi-1.md,/tmp/test-multi-2.md,/tmp/test-multi-3.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify multiple files results + run: | + echo "Files processed: ${{ steps.multi-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.multi-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.multi-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.multi-test.outputs.changes-made }}" + + if [ "${{ steps.multi-test.outputs.files-processed }}" != "3" ]; then + echo "āŒ Expected 3 files processed but got ${{ steps.multi-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.multi-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + # Only the first file (unformatted-code.md) should be changed + if [ "${{ steps.multi-test.outputs.files-changed }}" != "1" ]; then + echo "āŒ Expected 1 file changed but got ${{ steps.multi-test.outputs.files-changed }}" + exit 1 + fi + + # Should format 5 blocks from the unformatted file + if [ "${{ steps.multi-test.outputs.total-blocks-formatted }}" -lt "4" ]; then + echo "āŒ Expected at least 4 blocks formatted but got ${{ steps.multi-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Multiple files test passed" + + test-glob-patterns: + runs-on: ubuntu-latest + name: Test glob pattern support + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create test directory structure + mkdir -p /tmp/glob-test/subdir + + # Create test files with Python code + cat > /tmp/glob-test/file1.md << 'EOF' +# Test File 1 +```python +def test_function(x,y): + return x+y +``` +EOF + + cat > /tmp/glob-test/subdir/file2.md << 'EOF' +# Test File 2 +```{code-cell} python +def another_function(a,b): + result=a+b + return result +``` +EOF + + cat > /tmp/glob-test/not-markdown.txt << 'EOF' +This is not a markdown file +EOF + + - name: Test action with glob pattern + id: glob-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/glob-test/**/*.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify glob pattern results + run: | + echo "Files processed: ${{ steps.glob-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.glob-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.glob-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.glob-test.outputs.changes-made }}" + + if [ "${{ steps.glob-test.outputs.files-processed }}" != "2" ]; then + echo "āŒ Expected 2 files processed but got ${{ steps.glob-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.glob-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + if [ "${{ steps.glob-test.outputs.files-changed }}" != "2" ]; then + echo "āŒ Expected 2 files changed but got ${{ steps.glob-test.outputs.files-changed }}" + exit 1 + fi + + if [ "${{ steps.glob-test.outputs.total-blocks-formatted }}" != "2" ]; then + echo "āŒ Expected 2 blocks formatted but got ${{ steps.glob-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Glob pattern test passed" + + test-mixed-input: + runs-on: ubuntu-latest + name: Test mixed glob patterns and explicit files + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test environment + run: | + # Create test directory + mkdir -p /tmp/mixed-test + + # Create a test file with Python code + cat > /tmp/mixed-test/glob-file.md << 'EOF' +# Glob File +```python +def glob_function(x,y): + return x+y +``` +EOF + + # Use existing test file as explicit file + cp test/code-style-checker/unformatted-code.md /tmp/explicit-file.md + + - name: Test action with mixed input + id: mixed-test + uses: .//.github/actions/code-style-checker + with: + files: '/tmp/mixed-test/*.md,/tmp/explicit-file.md' + commit-files: 'false' # Don't commit during tests + + - name: Verify mixed input results + run: | + echo "Files processed: ${{ steps.mixed-test.outputs.files-processed }}" + echo "Files changed: ${{ steps.mixed-test.outputs.files-changed }}" + echo "Blocks formatted: ${{ steps.mixed-test.outputs.total-blocks-formatted }}" + echo "Changes made: ${{ steps.mixed-test.outputs.changes-made }}" + + if [ "${{ steps.mixed-test.outputs.files-processed }}" != "2" ]; then + echo "āŒ Expected 2 files processed but got ${{ steps.mixed-test.outputs.files-processed }}" + exit 1 + fi + + if [ "${{ steps.mixed-test.outputs.changes-made }}" != "true" ]; then + echo "āŒ Expected changes to be made but none were made" + exit 1 + fi + + if [ "${{ steps.mixed-test.outputs.files-changed }}" != "2" ]; then + echo "āŒ Expected 2 files changed but got ${{ steps.mixed-test.outputs.files-changed }}" + exit 1 + fi + + # Should format 1 block from glob file + at least 4 from explicit file + if [ "${{ steps.mixed-test.outputs.total-blocks-formatted }}" -lt "5" ]; then + echo "āŒ Expected at least 5 blocks formatted but got ${{ steps.mixed-test.outputs.total-blocks-formatted }}" + exit 1 + fi + + echo "āœ… Mixed input test passed" \ No newline at end of file diff --git a/README.md b/README.md index 8180c6c..e398524 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,29 @@ A GitHub Action that scans HTML files for Python warnings and optionally fails t See the [action documentation](./.github/actions/check-warnings/README.md) for detailed usage instructions and examples. +### Code Style Formatter Action + +A GitHub Action that automatically formats Python code in MyST markdown files and standard markdown code blocks using black. + +**Location**: `.github/actions/code-style-checker` + +**Usage**: +```yaml +- name: Format Python code in markdown files + uses: QuantEcon/meta/.github/actions/code-style-checker@main + with: + files: 'lecture/aiyagari.md,lecture/mccall.md' + check-myst-code-cells: 'true' + check-markdown-blocks: 'true' + python-languages: 'python,python3,ipython,ipython3' + black-args: '--line-length=88' + commit-files: 'true' +``` + +**Use case**: Perfect for maintaining consistent Python code formatting in MyST Markdown/Jupyter Book projects. Can be triggered by PR comments using `@quantecon-code-style` for on-demand formatting of changed files. + +See the [action documentation](./.github/actions/code-style-checker/README.md) for detailed usage instructions and examples. + ### AI-Powered Link Checker Action A GitHub Action that validates web links in HTML files with AI-powered suggestions for improvements. Designed to replace traditional link checkers like `lychee` with enhanced functionality. diff --git a/test/README.md b/test/README.md index e2d2597..d01cde0 100644 --- a/test/README.md +++ b/test/README.md @@ -10,6 +10,11 @@ Each GitHub Action has its own test subdirectory: - `clean.html` - HTML file without warnings (negative test case) - `with-warnings.html` - HTML file with warnings (positive test case) +- `code-style-checker/` - Tests for the `.github/actions/code-style-checker` action + - `unformatted-code.md` - Markdown file with poorly formatted Python code (positive test case) + - `formatted-code.md` - Markdown file with well-formatted Python code (negative test case) + - `no-python-code.md` - Markdown file with non-Python code blocks (negative test case) + - `link-checker/` - Tests for the `.github/actions/link-checker` action - `good-links.html` - HTML file with working external links (negative test case) - `broken-links.html` - HTML file with broken and problematic links (positive test case) @@ -22,5 +27,6 @@ Each GitHub Action has its own test subdirectory: 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 `code-style-checker` action, tests are run by the `test-code-style-checker.yml` workflow. - For the `link-checker` action, tests are run by the `test-link-checker.yml` workflow. - For the `weekly-report` action, tests are run by the `test-weekly-report.yml` workflow. diff --git a/test/code-style-checker/formatted-code.md b/test/code-style-checker/formatted-code.md new file mode 100644 index 0000000..e88bffe --- /dev/null +++ b/test/code-style-checker/formatted-code.md @@ -0,0 +1,79 @@ +# Test Markdown with Already Formatted Code + +This file contains properly formatted Python code to test that no changes are made. + +## MyST Code Cells + +```{code-cell} python +import numpy as np + + +def well_formatted_function(x, y): + result = x + y + return result + + +x = 5 +y = 10 +print(f"Result is {well_formatted_function(x, y)}") +``` + +```{code-cell} python3 +class WellFormattedClass: + def __init__(self, value): + self.value = value + + def get_value(self): + return self.value + + +obj = WellFormattedClass(42) +print(obj.get_value()) +``` + +## Standard Markdown Code Blocks + +```python +import matplotlib.pyplot as plt + + +def plot_data(x, y, title="Default Title"): + fig, ax = plt.subplots() + ax.plot(x, y) + ax.set_title(title) + return fig + + +data_x = [1, 2, 3, 4, 5] +data_y = [2, 4, 6, 8, 10] +plot_data(data_x, data_y, "My Plot") +``` + +```ipython +# This is an ipython code block +import pandas as pd + +df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) +print(df.head()) + +# Well formatted lambda +process_data = lambda x: x * 2 + 1 +result = process_data(5) +print(f"Result: {result}") +``` + +## Mixed Code Blocks + +```{code-cell} ipython3 +# This is already formatted +import numpy as np + + +def calculate_mean(arr): + return np.mean(arr) + + +data = np.array([1, 2, 3, 4, 5]) +mean_value = calculate_mean(data) +print(f"Mean: {mean_value}") +``` \ No newline at end of file diff --git a/test/code-style-checker/no-python-code.md b/test/code-style-checker/no-python-code.md new file mode 100644 index 0000000..6e1f191 --- /dev/null +++ b/test/code-style-checker/no-python-code.md @@ -0,0 +1,64 @@ +# Test Markdown with Only Non-Python Code + +This file contains no Python code blocks to test that nothing is processed. + +## Julia Code + +```{code-cell} julia +function calculate_fibonacci(n) + if n <= 1 + return n + else + return calculate_fibonacci(n-1) + calculate_fibonacci(n-2) + end +end + +result = calculate_fibonacci(10) +println("Fibonacci(10) = $result") +``` + +## JavaScript Code + +```javascript +function calculateSum(arr) { + return arr.reduce((sum, num) => sum + num, 0); +} + +const numbers = [1, 2, 3, 4, 5]; +const total = calculateSum(numbers); +console.log(`Total: ${total}`); +``` + +## R Code + +```r +# Calculate mean and standard deviation +data <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +mean_value <- mean(data) +sd_value <- sd(data) + +print(paste("Mean:", mean_value)) +print(paste("Standard Deviation:", sd_value)) +``` + +## Plain Text + +``` +This is just plain text in a code block. +No language is specified. +Should be ignored by the formatter. +``` + +## YAML + +```yaml +name: Example YAML +description: This is a YAML code block +config: + setting1: value1 + setting2: value2 + numbers: + - 1 + - 2 + - 3 +``` \ No newline at end of file diff --git a/test/code-style-checker/unformatted-code.md b/test/code-style-checker/unformatted-code.md new file mode 100644 index 0000000..c3cbc20 --- /dev/null +++ b/test/code-style-checker/unformatted-code.md @@ -0,0 +1,101 @@ +# Test Markdown with Unformatted Python Code + +This file contains Python code that needs formatting to test the code style checker. + +## MyST Code Cells + +```{code-cell} python +import numpy as np +def badly_formatted_function( x,y ): + result=x+y + return result + +x=5;y=10 +print(f"Result is {badly_formatted_function(x,y)}") +``` + +```{code-cell} python3 +class BadlyFormattedClass: + def __init__(self,value): + self.value=value + def get_value( self ): + return self.value + +obj=BadlyFormattedClass(42) +print( obj.get_value() ) +``` + +## Standard Markdown Code Blocks + +```python +import matplotlib.pyplot as plt + +def plot_data(x,y,title="Default Title"): + fig,ax=plt.subplots() + ax.plot(x,y) + ax.set_title(title) + return fig + +data_x=[1,2,3,4,5] +data_y=[2,4,6,8,10] +plot_data(data_x,data_y,"My Plot") +``` + +```ipython +# This is an ipython code block +import pandas as pd + +df=pd.DataFrame({"A":[1,2,3],"B":[4,5,6]}) +print(df.head()) + +# Badly formatted lambda +process_data=lambda x: x*2+1 +result=process_data(5) +print(f"Result: {result}") +``` + +## Non-Python Code Blocks (Should be skipped) + +```julia +# This Julia code should not be formatted +function badly_formatted_julia(x,y) + return x+y +end + +result=badly_formatted_julia(1,2) +println("Result: $result") +``` + +```javascript +// This JavaScript code should not be formatted +function badlyFormattedJS(x,y){ +return x+y; +} + +let result=badlyFormattedJS(1,2); +console.log("Result: "+result); +``` + +## Mixed Code Blocks + +```{code-cell} ipython3 +# This should be formatted +import numpy as np + +def calculate_mean( arr ): + return np.mean(arr) + +data=np.array([1,2,3,4,5]) +mean_value=calculate_mean(data) +print(f"Mean: {mean_value}") +``` + +```{code-cell} julia +# This Julia code should be skipped +function calculate_sum(x, y) + return x + y +end + +result = calculate_sum(10, 20) +println("Sum: $result") +``` \ No newline at end of file