Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 360 additions & 0 deletions .github/actions/code-style-checker/README.md
Original file line number Diff line number Diff line change
@@ -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: '[email protected]'

- 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 | ❌ | `[email protected]` |

## 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)
Loading