A reusable GitHub Actions workflow that chips away at formatting and linting violations over time. Instead of one massive PR that touches every file, it creates small, reviewable pull requests on a schedule — each staying within a configurable line-change budget.
| Tool | Language | What it does |
|---|---|---|
| RuboCop | Ruby | Fixes the highest-volume correctable cop first (up to 3 attempts) |
| stree | Ruby | Applies syntax-tree formatting |
| Prettier | JS / TS | Applies Prettier formatting to .js, .jsx, .ts, .tsx files |
| ESLint + Prettier | JS / TS | When eslint-plugin-prettier is detected, uses eslint --fix instead of standalone Prettier |
Each tool is optional — the workflow skips any tool that isn't installed or configured in the calling repository.
Create a workflow in your repository (e.g. .github/workflows/progressive-improvements.yml):
name: Progressive Improvements
on:
schedule:
- cron: '0 14 * * 1' # Every Monday at 2 PM UTC
workflow_dispatch: # Allow manual runs
permissions:
contents: write
pull-requests: write
jobs:
improve:
uses: planningcenter/gh-action-progressive-improvements/.github/workflows/improvement.yml@v1That's it. The workflow will:
- Detect which tools are available in your project
- Run each tool and collect fixes
- Trim changes to stay within the line budget
- Open (or update) a pull request with a detailed summary
All inputs are optional and have sensible defaults.
| Input | Type | Default | Description |
|---|---|---|---|
rubocop_max_lines |
string | 300 |
Max lines changed for RuboCop fixes |
stree_max_lines |
string | 200 |
Max lines changed for stree formatting |
stree_ignore_files |
string | '' |
Additional file patterns to exclude from stree formatting (space-separated globs) |
prettier_max_lines |
string | 200 |
Max lines changed for Prettier formatting |
rubocop_autocorrect_mode |
string | -a |
-a for safe auto-correct, -A for all (including unsafe) |
branch_prefix |
string | improvement |
Prefix for the created branch (e.g. improvement/2025-02-26-1400) |
pr_title_prefix |
string | chore(tidy): formatting and linting |
PR title |
pr_assignees |
string | '' |
Comma-separated GitHub usernames to assign to the PR |
pr_reviewers |
string | '' |
Comma-separated GitHub usernames to request review from |
dry_run |
boolean | false |
When true, analyzes and logs changes but does not commit or open a PR |
permissions:
contents: write
pull-requests: write
jobs:
improve:
uses: planningcenter/gh-action-progressive-improvements/.github/workflows/improvement.yml@v1
with:
rubocop_max_lines: '500'
stree_max_lines: '300'
prettier_max_lines: '300'
rubocop_autocorrect_mode: '-A'
stree_ignore_files: 'lib/generated/**/*.rb app/templates/**/*.rb'
dry_run: trueLet one person volunteer to review all improvement PRs before rolling it out to the whole team:
permissions:
contents: write
pull-requests: write
jobs:
improve:
uses: planningcenter/gh-action-progressive-improvements/.github/workflows/improvement.yml@v1
with:
pr_assignees: 'octocat'
pr_reviewers: 'octocat'Each tool has a maximum line-change budget. When a tool's changes exceed the budget, the workflow:
- Calculates a proportional subset of files that should fit within the budget
- Resets the working tree and re-runs the tool on only those files
- If still over budget, applies a second trim
This ensures PRs stay small and reviewable regardless of how many violations exist in the codebase.
Rather than fixing all cops at once, the workflow:
- Runs RuboCop in JSON mode to identify all correctable offenses
- Ranks cops by violation count (highest first)
- Attempts to fix the top cop — if it produces no changes, tries the next one (up to 3 attempts)
This focuses each PR on a single type of fix, making review easier.
The workflow auto-detects your project setup:
- Ruby: Runs
bundle exec rubocop/bundle exec streeif available, falls back to system commands - Node.js: Detects your package manager (yarn, pnpm, bun, or npm) and installs dependencies
- Prettier: Checks
node_modules/.bin/prettier, systemprettier, thennpx prettier; also requires a Prettier config file orprettierkey inpackage.json - ESLint + Prettier: If
eslint-plugin-prettieris installed innode_modules, the workflow useseslint --fixinstead of standalone Prettier. This ensures formatting matches what developers get locally when ESLint-specific Prettier overrides exist.
The workflow automatically excludes vendored dependencies from scanning:
- stree: Skips
vendor/**/*.rb,db/schema.rb, anddb/migrate/**/*.rbvia--ignore-files. Add extra patterns with thestree_ignore_filesinput. - Prettier: Skips
node_modules/(Prettier default) andvendor/ - RuboCop: Respects exclusions from your
.rubocop.ymlvia--force-exclusion
This avoids wasting time scanning third-party code in directories like vendor/bundle/.
If your project uses both RuboCop and syntax_tree (stree), the two tools can conflict on formatting rules. In normal development this rarely surfaces, but when the action runs both tools back-to-back across the entire codebase, the conflict can create a loop of identical fix PRs — each tool undoing the other's changes.
Recommended fix: Add syntax_tree's RuboCop config to your .rubocop.yml:
inherit_gem:
syntax_tree: config/rubocop.ymlThis disables the RuboCop cops that conflict with stree's formatting. See the syntax_tree RuboCop docs for details.
The action will emit a warning if it detects both tools are active without this configuration. It also tracks a diff fingerprint across runs and will skip creating a PR if the same changes were already merged recently.
The workflow requires these permissions on the caller's GITHUB_TOKEN:
permissions:
contents: write # Push branches
pull-requests: write # Create and update PRsThese are declared in the reusable workflow itself, but adding them explicitly to your calling workflow follows the principle of least privilege and makes the required scopes visible at a glance.
Each PR includes a structured summary showing what was changed. The JS/TS section will show either ESLint + Prettier or Prettier depending on your project setup:
## chore(tidy): formatting and linting
### RuboCop: `Style/StringLiterals`
- Fixed 12 files (87 lines changed)
- Safe auto-correct (`-a`)
### stree formatting
- Formatted 5 of 23 non-conforming Ruby files
### ESLint + Prettier formatting
- Fixed 8 JS/TS files via `eslint --fix` (eslint-plugin-prettier)
- 42 lines changed
---
Existing CI gates merging. Generated by improvement workflow.Use dry_run: true to see what the workflow would do without creating any commits or PRs. The workflow will still run all tools and log the results — useful for evaluating the scope of changes before going live.
This action uses GitHub Releases for version pinning. We recommend referencing a major version tag:
uses: planningcenter/gh-action-progressive-improvements/.github/workflows/improvement.yml@v1@v1— Follows the latestv1.x.xrelease (recommended). You get bug fixes and new features automatically, with no breaking changes.@v1.2.3— Pins to an exact release. Use this if you need full reproducibility.
For each tool to run, the calling repository needs:
- RuboCop:
.rubocop.yml+ RuboCop in the bundle - stree:
streein the bundle or onPATH - Prettier: A Prettier config file +
prettieravailable (vianode_modules, system, ornpx) - ESLint + Prettier:
eslint-plugin-prettierinnode_modules+eslintavailable (vianode_modules, system, ornpx). When detected, this takes precedence over standalone Prettier.