ruff-sync 0.1.3.dev1__tar.gz → 0.1.3.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/ruff-sync-usage/SKILL.md +12 -2
  2. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/ruff-sync-usage/references/ci-integration.md +4 -3
  3. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/ruff-sync-usage/references/configuration.md +1 -1
  4. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.github/workflows/ci.yaml +39 -1
  5. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/AGENTS.md +2 -0
  6. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/PKG-INFO +6 -3
  7. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/README.md +5 -2
  8. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/codecov.yml +1 -1
  9. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/ci-integration.md +5 -2
  10. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/usage.md +5 -0
  11. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/pyproject.toml +7 -1
  12. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/cli.py +21 -5
  13. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/constants.py +17 -0
  14. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/core.py +60 -32
  15. ruff_sync-0.1.3.dev2/src/ruff_sync/formatters.py +243 -0
  16. ruff_sync-0.1.3.dev2/tests/conftest.py +64 -0
  17. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_basic.py +157 -1
  18. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_check.py +230 -8
  19. ruff_sync-0.1.3.dev2/tests/test_formatters.py +154 -0
  20. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_scaffold.py +19 -7
  21. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_serialization.py +2 -1
  22. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/uv.lock +4 -4
  23. ruff_sync-0.1.3.dev1/tests/conftest.py +0 -12
  24. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/TESTING.md +0 -0
  25. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
  26. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/examples.md +0 -0
  27. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
  28. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
  29. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
  30. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
  31. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/release-notes-generation/SKILL.md +0 -0
  32. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
  33. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.agents/workflows/add-test-case.md +0 -0
  34. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.git-blame-ignore-revs +0 -0
  35. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.github/dependabot.yml +0 -0
  36. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.github/workflows/complexity.yaml +0 -0
  37. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.github/workflows/docs.yaml +0 -0
  38. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.gitignore +0 -0
  39. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.pre-commit-config.yaml +0 -0
  40. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/.pre-commit-hooks.yaml +0 -0
  41. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/CONTRIBUTING.md +0 -0
  42. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/LICENSE.md +0 -0
  43. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/configs/data-science-engineering/ruff.toml +0 -0
  44. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/configs/fastapi/ruff.toml +0 -0
  45. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/configs/kitchen-sink/ruff.toml +0 -0
  46. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/agent-skill.md +0 -0
  47. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/assets/favicon.png +0 -0
  48. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/assets/logo.png +0 -0
  49. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/assets/ruff_sync_banner.png +0 -0
  50. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/best-practices.md +0 -0
  51. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/configuration.md +0 -0
  52. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/contributing.md +0 -0
  53. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/examples/advanced-config.toml +0 -0
  54. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/examples/basic-config.toml +0 -0
  55. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/gen_ref_pages.py +0 -0
  56. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/index.md +0 -0
  57. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/installation.md +0 -0
  58. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/pre-commit.md +0 -0
  59. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/pre-defined-configs.md +0 -0
  60. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/troubleshooting.md +0 -0
  61. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/docs/url-resolution.md +0 -0
  62. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/mkdocs.yml +0 -0
  63. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/scripts/check_dogfood.sh +0 -0
  64. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/scripts/gitclone_dogfood.sh +0 -0
  65. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/scripts/pull_dogfood.sh +0 -0
  66. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/skills-lock.json +0 -0
  67. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/__init__.py +0 -0
  68. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/__main__.py +0 -0
  69. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/src/ruff_sync/pre_commit.py +0 -0
  70. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tasks.py +0 -0
  71. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/__init__.py +0 -0
  72. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
  73. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
  74. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
  75. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
  76. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  77. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  78. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  79. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  80. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  81. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  82. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  83. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  84. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  85. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  86. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  87. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  88. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/standard_final.toml +0 -0
  89. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  90. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  91. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/ruff.toml +0 -0
  92. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_config_validation.py +0 -0
  93. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_constants.py +0 -0
  94. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_corner_cases.py +0 -0
  95. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_deprecation.py +0 -0
  96. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_e2e.py +0 -0
  97. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_git_fetch.py +0 -0
  98. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_pre_commit.py +0 -0
  99. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_project.py +0 -0
  100. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_toml_operations.py +0 -0
  101. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_url_handling.py +0 -0
  102. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/test_whitespace.py +0 -0
  103. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  104. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  105. {ruff_sync-0.1.3.dev1 → ruff_sync-0.1.3.dev2}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -77,8 +77,9 @@ upstream = [
77
77
  CI Setup Progress:
78
78
  - [ ] 1. Add ruff-sync check step to CI workflow (see references/ci-integration.md)
79
79
  - [ ] 2. Decide: --semantic for value-only checks, or full string comparison
80
- - [ ] 3. Set exit-code expectations (0 = in sync, 1 = config drift, 2 = pre-commit only)
81
- - [ ] 4. Verify locally: `ruff-sync check --semantic`
80
+ - [ ] 3. Set output format: --output-format github for PR annotations
81
+ - [ ] 4. Set exit-code expectations (0 = in sync, 1 = config drift, 2 = pre-commit only)
82
+ - [ ] 5. Verify locally: `ruff-sync check --semantic`
82
83
  ```
83
84
 
84
85
  Keep the `ruff-pre-commit` hook version in `.pre-commit-config.yaml` aligned with the project's Ruff version.
@@ -112,6 +113,15 @@ ruff-sync https://raw.githubusercontent.com/my-org/standards/main/pyproject.toml
112
113
  ruff-sync git@github.com:my-org/standards.git # SSH (shallow clone)
113
114
  ```
114
115
 
116
+ ## CLI Reference (Short)
117
+
118
+ | Flag | Meaning |
119
+ |------|---------|
120
+ | `--output-format` | `text` (default), `json`, `github` (PR annotations) |
121
+ | `--semantic` | Ignore whitespace/comments in `check` |
122
+ | `--pre-commit` | Sync `.pre-commit-config.yaml` hook version |
123
+ | `--save` | Persist CLI args to `pyproject.toml` |
124
+
115
125
  ## Gotchas
116
126
 
117
127
  - **`exclude` uses dotted paths, not TOML paths.** `lint.per-file-ignores` refers to the `per-file-ignores` key inside `[tool.ruff.lint]`. Do NOT write `tool.ruff.lint.per-file-ignores`.
@@ -8,10 +8,11 @@ Add this step to any existing workflow (e.g., `.github/workflows/ci.yaml`):
8
8
 
9
9
  ```yaml
10
10
  - name: Check Ruff config is in sync
11
- run: ruff-sync check --semantic
11
+ run: ruff-sync check --semantic --output-format github
12
12
  ```
13
13
 
14
14
  `--semantic` ignores cosmetic differences (comments, whitespace) — only real value or rule changes cause failure.
15
+ `--output-format github` creates inline PR annotations for errors and warnings.
15
16
 
16
17
  ### Full Workflow Example
17
18
 
@@ -40,7 +41,7 @@ jobs:
40
41
  run: uv tool install ruff-sync
41
42
 
42
43
  - name: Check Ruff config is in sync with upstream
43
- run: ruff-sync check --semantic
44
+ run: ruff-sync check --semantic --output-format github
44
45
  ```
45
46
 
46
47
  ### With Pre-commit Sync Check
@@ -49,7 +50,7 @@ To also verify the pre-commit hook version, add the `--pre-commit` flag. Any non
49
50
 
50
51
  ```yaml
51
52
  - name: Check Ruff config and pre-commit hook
52
- run: ruff-sync check --semantic --pre-commit
53
+ run: ruff-sync check --semantic --pre-commit --output-format github
53
54
  ```
54
55
 
55
56
  (Note: For better consistency, you can instead set `pre-commit-version-sync = true` in your `pyproject.toml` — then `ruff-sync check --semantic` will automatically include this check.)
@@ -127,7 +127,7 @@ pre-commit-version-sync = true
127
127
  All config keys have CLI equivalents. CLI values always win over `pyproject.toml`:
128
128
 
129
129
  ```bash
130
- ruff-sync --exclude lint.ignore --branch develop https://github.com/my-org/standards
130
+ ruff-sync https://github.com/my-org/standards --exclude lint.ignore --branch develop --output-format github
131
131
  ```
132
132
 
133
133
  ## Config Discovery for `ruff.toml` Projects
@@ -31,6 +31,26 @@ jobs:
31
31
 
32
32
  - run: uv run invoke ${{ matrix.task }} --check
33
33
 
34
+ validate-docs-build:
35
+ name: Validate documentation build
36
+ if: github.event_name == 'pull_request'
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v4
41
+
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@v5
44
+
45
+ - name: Set up Python
46
+ run: uv python install 3.10
47
+
48
+ - name: Install dependencies
49
+ run: uv sync --group docs --frozen
50
+
51
+ - name: Build documentation
52
+ run: uv run mkdocs build --strict
53
+
34
54
  tests:
35
55
  strategy:
36
56
  fail-fast: ${{ github.event.pull_request.draft == true }}
@@ -102,6 +122,13 @@ jobs:
102
122
  echo "Testing ruff-sync check against Kilo59/ruff-sync..."
103
123
  ruff-sync check https://github.com/Kilo59/ruff-sync
104
124
 
125
+ - name: Dogfood Ruff-Sync Check (GitHub Format)
126
+ run: |
127
+ echo "Dogfooding ruff-sync check with GitHub annotations..."
128
+ # This will produce annotations if the current branch's ruff config
129
+ # has drifted from the upstream (main branch).
130
+ ruff-sync check https://github.com/Kilo59/ruff-sync --output-format github
131
+
105
132
  - name: Verify semantic check failure
106
133
  run: |
107
134
  echo "Verifying that --semantic check fails for kitchen-sink config (expected failure)..."
@@ -131,4 +158,15 @@ jobs:
131
158
  run: uv build
132
159
 
133
160
  - name: Publish package
134
- run: uv publish
161
+ run: |
162
+ # Capture stderr to a file so we can check for specific error strings
163
+ uv publish 2> publish_err.log || {
164
+ exit_code=$?
165
+ # Check for common "version already exists" markers in the output
166
+ if grep -qi "already exists" publish_err.log; then
167
+ echo "::warning title=Ruff Sync Publish::Version already exists on PyPI. Skipping upload."
168
+ else
169
+ cat publish_err.log
170
+ exit $exit_code
171
+ fi
172
+ }
@@ -158,6 +158,7 @@ uv run coverage run -m pytest -vv
158
158
  - Prefer f-strings for logging (we ignore `G004`).
159
159
  - Do not create custom exception classes for simple errors (`TRY003` is ignored).
160
160
  - **Prefer `NamedTuple` for return types** over plain tuples to improve readability and type safety.
161
+ - **Prefer `typing.Protocol` over `abc.ABC`** for abstract base classes to promote structural subtyping.
161
162
 
162
163
  ### TOML Handling
163
164
 
@@ -219,3 +220,4 @@ CI is defined in `.github/workflows/ci.yaml`:
219
220
  2. **`cast(Any, ...)` in tests**: Use `cast(Any, tomlkit.parse(...))["tool"]["ruff"]` pattern in tests to avoid mypy complaints about `tomlkit`'s `Item | Container` return types.
220
221
  3. **Pre-commit ruff version**: The ruff version in `.pre-commit-config.yaml` must stay in sync with the version in `pyproject.toml`. The test `test_pre_commit_versions_are_in_sync` enforces this.
221
222
  4. **Keep the ruff-sync-usage skill current**: After any change to CLI behavior (new flags, changed exit codes, new configuration keys, updated URL handling, etc.), update `.agents/skills/ruff-sync-usage/` accordingly. The `SKILL.md` covers quick start, workflows, exit codes, and gotchas. Detailed references live in `references/configuration.md`, `references/troubleshooting.md`, and `references/ci-integration.md`.
223
+ 5. **No `autouse=True` fixtures**: NEVER use `autouse=True` for pytest fixtures. All fixtures must be explicitly requested by the test functions that require them. This ensures dependencies are explicit and avoids hidden side effects.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruff-sync
3
- Version: 0.1.3.dev1
3
+ Version: 0.1.3.dev2
4
4
  Summary: Synchronize Ruff linter configuration across projects
5
5
  Project-URL: Homepage, https://github.com/Kilo59/ruff-sync
6
6
  Project-URL: Documentation, https://kilo59.github.io/ruff-sync/
@@ -208,6 +208,7 @@ See the [Usage documentation](https://kilo59.github.io/ruff-sync/usage/) for mor
208
208
  - 🧠 **Semantic mode** — Use `--semantic` to ignore cosmetic differences (comments, whitespace) and only fail on real value changes.
209
209
  - 🔗 **Pre-commit hook sync** — Use `--pre-commit` to automatically keep your `ruff-pre-commit` hook version in `.pre-commit-config.yaml` matching your project's Ruff version.
210
210
  - 🦾 **Agent Skill** — Ships a bundled [Agent Skill](https://kilo59.github.io/ruff-sync/agent-skill/) so AI coding agents can guide you through setup, configuration, and troubleshooting automatically.
211
+ - 📊 **Multiple Output Formats** — Supports `text`, `json`, and GitHub Actions `github` (inline annotations) formats for seamless integration with both human developers and CI/CD pipelines.
211
212
 
212
213
  ## Configuration
213
214
 
@@ -289,7 +290,7 @@ The `check` command is designed for use in CI pipelines. Add it as a step to cat
289
290
  # .github/workflows/ci.yaml
290
291
  - name: Check ruff config is in sync
291
292
  run: |
292
- ruff-sync check --semantic
293
+ ruff-sync check --semantic --output-format github
293
294
  ```
294
295
 
295
296
  With `--semantic`, minor reformatting of your local file won't cause a false positive — only actual rule or value differences will fail the check.
@@ -442,7 +443,9 @@ flowchart TD
442
443
 
443
444
  ## Dogfooding
444
445
 
445
- To see `ruff-sync` in action, you can ["dogfood" it on this project's own config](./scripts).
446
+ To see `ruff-sync` in action, this project automatically "dogfoods" its own configuration. Every pull request runs a `ruff-sync check` against the repository's own `pyproject.toml` using the `--output-format github` flag, providing real-time feedback and inline annotations whenever configuration drift is detected.
447
+
448
+ You can also run these checks manually or experiment with syncing:
446
449
 
447
450
  [**Check if this project is in sync with its upstream:**](./scripts/check_dogfood.sh)
448
451
 
@@ -177,6 +177,7 @@ See the [Usage documentation](https://kilo59.github.io/ruff-sync/usage/) for mor
177
177
  - 🧠 **Semantic mode** — Use `--semantic` to ignore cosmetic differences (comments, whitespace) and only fail on real value changes.
178
178
  - 🔗 **Pre-commit hook sync** — Use `--pre-commit` to automatically keep your `ruff-pre-commit` hook version in `.pre-commit-config.yaml` matching your project's Ruff version.
179
179
  - 🦾 **Agent Skill** — Ships a bundled [Agent Skill](https://kilo59.github.io/ruff-sync/agent-skill/) so AI coding agents can guide you through setup, configuration, and troubleshooting automatically.
180
+ - 📊 **Multiple Output Formats** — Supports `text`, `json`, and GitHub Actions `github` (inline annotations) formats for seamless integration with both human developers and CI/CD pipelines.
180
181
 
181
182
  ## Configuration
182
183
 
@@ -258,7 +259,7 @@ The `check` command is designed for use in CI pipelines. Add it as a step to cat
258
259
  # .github/workflows/ci.yaml
259
260
  - name: Check ruff config is in sync
260
261
  run: |
261
- ruff-sync check --semantic
262
+ ruff-sync check --semantic --output-format github
262
263
  ```
263
264
 
264
265
  With `--semantic`, minor reformatting of your local file won't cause a false positive — only actual rule or value differences will fail the check.
@@ -411,7 +412,9 @@ flowchart TD
411
412
 
412
413
  ## Dogfooding
413
414
 
414
- To see `ruff-sync` in action, you can ["dogfood" it on this project's own config](./scripts).
415
+ To see `ruff-sync` in action, this project automatically "dogfoods" its own configuration. Every pull request runs a `ruff-sync check` against the repository's own `pyproject.toml` using the `--output-format github` flag, providing real-time feedback and inline annotations whenever configuration drift is detected.
416
+
417
+ You can also run these checks manually or experiment with syncing:
415
418
 
416
419
  [**Check if this project is in sync with its upstream:**](./scripts/check_dogfood.sh)
417
420
 
@@ -6,4 +6,4 @@ flag_management:
6
6
  target: auto # the default target for the project
7
7
  threshold: 75% # the default minimum threshold for the project
8
8
  - type: patch
9
- target: 95%
9
+ target: 90%
@@ -25,10 +25,13 @@ jobs:
25
25
  steps:
26
26
  - uses: actions/checkout@v4
27
27
  - uses: astral-sh/setup-uv@v5
28
- - run: uvx ruff-sync check --semantic
28
+ - name: Check Ruff Config
29
+ run: uvx ruff-sync check --semantic --output-format github
29
30
  ```
30
31
 
31
- #### Automated Sync PRs
32
+ By using `--output-format github`, `ruff-sync` will emit special workflow commands that GitHub translates into inline annotations directly on your Pull Request's file diff.
33
+
34
+ ### Automated Sync PRs
32
35
 
33
36
  Instead of just checking, you can have a bot automatically open a PR when the upstream configuration changes.
34
37
 
@@ -165,6 +165,10 @@ ruff-sync [UPSTREAM_URL...] [--to PATH] [--exclude KEY...] [--init] [--pre-commi
165
165
  * **`--exclude KEY...`**: Dotted paths of keys to keep local and never overwrite (e.g., `lint.isort`).
166
166
  * **`--init`**: Create a new `pyproject.toml` with the upstream configuration if it doesn't already exist. This automatically saves the upstream source and any other CLI flags into the `[tool.ruff-sync]` section.
167
167
  * **`--save` / `--no-save`**: Force serialization (or prevent serialization) of the provided CLI arguments (like `upstream`, `exclude`, etc.) directly into the `[tool.ruff-sync]` section of the target `pyproject.toml` for future use. Note: any credentials present in the upstream URL will cause this operation to safely abort.
168
+ * **`--output-format [text|json|github]`**: Specify the output format for synchronization results.
169
+ - `text` (default): Human-readable terminal output.
170
+ - `json`: Machine-readable JSON output for tool integration.
171
+ - `github`: GitHub Actions workflow commands (`::error`, `::warning`) for inline PR annotations.
168
172
  * **`--pre-commit`**: Sync the `astral-sh/ruff-pre-commit` hook version inside `.pre-commit-config.yaml` with the project's Ruff version.
169
173
 
170
174
  ### `check`
@@ -182,6 +186,7 @@ ruff-sync check [UPSTREAM_URL...] [--semantic] [--diff] [--pre-commit]
182
186
  * **`UPSTREAM_URL...`**: The source URL(s). Optional if defined locally.
183
187
  * **`--semantic`**: Ignore "non-functional" differences like whitespace, comments, or key order. Only errors if the actual Python-level data differs.
184
188
  * **`--diff` / `--no-diff`**: Control the display of the unified diff in the terminal.
189
+ * **`--output-format [text|json|github]`**: Specify the output format for check results. Defaults to `text`.
185
190
  * **`--pre-commit`**: Verify that the `astral-sh/ruff-pre-commit` hook version matches the project's Ruff version in addition to checking configuration drift. If you have `pre-commit-version-sync = true` configured in your `pyproject.toml`, the `check` command will automatically respect this setting and you do not need to pass this flag.
186
191
 
187
192
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.1.3.dev1"
3
+ version = "0.1.3.dev2"
4
4
  description = "Synchronize Ruff linter configuration across projects"
5
5
  keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit", "pre-commit"]
6
6
  authors = [
@@ -162,6 +162,8 @@ select = [
162
162
  "TID", # flake8-tidy-imports
163
163
  # https://docs.astral.sh/ruff/rules/#tryceratops-try
164
164
  "TRY", # tryceratops - exception handling
165
+ # https://docs.astral.sh/ruff/rules/#flake8-print-t20
166
+ "T20", # flake8-print
165
167
  # https://beta.ruff.rs/docs/rules/#pyupgrade-up
166
168
  "UP", # pyupgrade
167
169
  ]
@@ -173,8 +175,12 @@ ignore = [
173
175
  ]
174
176
 
175
177
  [tool.ruff.lint.per-file-ignores]
178
+ "src/ruff_sync/formatters.py" = ["T20"]
179
+ "tasks.py" = ["T20"]
176
180
  "tests/**/*.py" = [
181
+ "T20", # allow print in tests
177
182
  "D100", # missing docstring in module
183
+ "D101", # missing docstring in class
178
184
  "D103", # missing docstring in function
179
185
  "D400", # first line should end with a period
180
186
  "D401", # first line of docstring should be in imperative mood
@@ -33,6 +33,7 @@ from ruff_sync.constants import (
33
33
  DEFAULT_PATH,
34
34
  MISSING,
35
35
  MissingType,
36
+ OutputFormat,
36
37
  resolve_defaults,
37
38
  )
38
39
  from ruff_sync.core import (
@@ -102,6 +103,7 @@ class Arguments(NamedTuple):
102
103
  init: bool = False
103
104
  pre_commit: bool | MissingType = MISSING
104
105
  save: bool | None = None
106
+ output_format: OutputFormat = OutputFormat.TEXT
105
107
 
106
108
  @property
107
109
  @deprecated("Use 'to' instead")
@@ -249,6 +251,13 @@ def _get_cli_parser() -> ArgumentParser:
249
251
  default=None,
250
252
  help="Sync the pre-commit Ruff hook version with the project's Ruff version.",
251
253
  )
254
+ common_parser.add_argument(
255
+ "--output-format",
256
+ type=OutputFormat,
257
+ choices=list(OutputFormat),
258
+ default=OutputFormat.TEXT,
259
+ help="Format for output. Default: text.",
260
+ )
252
261
 
253
262
  # Pull subcommand (the default action)
254
263
  pull_parser = subparsers.add_parser(
@@ -433,11 +442,17 @@ def main() -> int:
433
442
  1: logging.INFO,
434
443
  }.get(args.verbose, logging.DEBUG)
435
444
 
436
- LOGGER.setLevel(log_level)
437
- handler = logging.StreamHandler()
438
- handler.setFormatter(ColoredFormatter())
439
- LOGGER.addHandler(handler)
440
- LOGGER.propagate = "PYTEST_CURRENT_TEST" in os.environ # Allow capturing in tests
445
+ # Configure logging for the entire ruff_sync package
446
+ root_logger = logging.getLogger("ruff_sync")
447
+ root_logger.setLevel(log_level)
448
+
449
+ # Avoid adding multiple handlers if main() is called multiple times (e.g. in tests)
450
+ if not root_logger.handlers:
451
+ handler = logging.StreamHandler()
452
+ handler.setFormatter(ColoredFormatter())
453
+ root_logger.addHandler(handler)
454
+
455
+ root_logger.propagate = "PYTEST_CURRENT_TEST" in os.environ
441
456
 
442
457
  # Determine target 'to' from CLI or use default '.'
443
458
  # Defer Path conversion to avoid pyfakefs issues with captured Path class
@@ -473,6 +488,7 @@ def main() -> int:
473
488
  init=getattr(args, "init", False),
474
489
  pre_commit=pre_commit_val,
475
490
  save=getattr(args, "save", None),
491
+ output_format=getattr(args, "output_format", OutputFormat.TEXT),
476
492
  )
477
493
 
478
494
  # Use the shared helper from constants so the MISSING→default logic for
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import enum
6
6
  from typing import TYPE_CHECKING, Final
7
7
 
8
+ from typing_extensions import override
9
+
8
10
  if TYPE_CHECKING:
9
11
  from collections.abc import Iterable
10
12
 
@@ -14,6 +16,7 @@ __all__: Final[list[str]] = [
14
16
  "DEFAULT_PATH",
15
17
  "MISSING",
16
18
  "MissingType",
19
+ "OutputFormat",
17
20
  "resolve_defaults",
18
21
  ]
19
22
 
@@ -42,6 +45,20 @@ class MissingType(enum.Enum):
42
45
  MISSING: Final[MissingType] = MissingType.SENTINEL
43
46
 
44
47
 
48
+ @enum.unique
49
+ class OutputFormat(str, enum.Enum):
50
+ """Output formats for the CLI."""
51
+
52
+ TEXT = "text"
53
+ JSON = "json"
54
+ GITHUB = "github"
55
+
56
+ @override
57
+ def __str__(self) -> str:
58
+ """Return the string value for argparse help."""
59
+ return self.value
60
+
61
+
45
62
  def resolve_defaults(
46
63
  branch: str | MissingType,
47
64
  path: str | MissingType,
@@ -38,6 +38,7 @@ from ruff_sync.constants import (
38
38
  MISSING,
39
39
  resolve_defaults,
40
40
  )
41
+ from ruff_sync.formatters import ResultFormatter, get_formatter
41
42
  from ruff_sync.pre_commit import sync_pre_commit
42
43
 
43
44
  if TYPE_CHECKING:
@@ -116,6 +117,16 @@ class UpstreamError(Exception):
116
117
  super().__init__(msg)
117
118
 
118
119
 
120
+ class DiffContext(NamedTuple):
121
+ """Context for printing a diff between local and merged configurations."""
122
+
123
+ source_toml_path: pathlib.Path
124
+ source_doc: TOMLDocument
125
+ merged_doc: TOMLDocument
126
+ source_val: Any
127
+ merged_val: Any
128
+
129
+
119
130
  class Config(TypedDict, total=False):
120
131
  """Configuration schema for [tool.ruff-sync] in pyproject.toml."""
121
132
 
@@ -837,25 +848,22 @@ async def _merge_multiple_upstreams(
837
848
 
838
849
  def _print_diff(
839
850
  args: Arguments,
840
- source_toml_path: pathlib.Path,
841
- source_doc: tomlkit.TOMLDocument,
842
- merged_doc: tomlkit.TOMLDocument,
843
- source_val: Any,
844
- merged_val: Any,
851
+ fmt: ResultFormatter,
852
+ ctx: DiffContext,
845
853
  ) -> None:
846
854
  """Print the unified diff between the local and expected configurations."""
847
855
  if args.semantic:
848
856
  # Semantic diff of the managed section
849
- from_lines = json.dumps(source_val, indent=2, sort_keys=True).splitlines(keepends=True)
850
- to_lines = json.dumps(merged_val, indent=2, sort_keys=True).splitlines(keepends=True)
857
+ from_lines = json.dumps(ctx.source_val, indent=2, sort_keys=True).splitlines(keepends=True)
858
+ to_lines = json.dumps(ctx.merged_val, indent=2, sort_keys=True).splitlines(keepends=True)
851
859
  from_file = "local (semantic)"
852
860
  to_file = "upstream (semantic)"
853
861
  else:
854
862
  # Full text diff of the file
855
- from_lines = source_doc.as_string().splitlines(keepends=True)
856
- to_lines = merged_doc.as_string().splitlines(keepends=True)
857
- from_file = f"local/{source_toml_path.name}"
858
- to_file = f"upstream/{source_toml_path.name}"
863
+ from_lines = ctx.source_doc.as_string().splitlines(keepends=True)
864
+ to_lines = ctx.merged_doc.as_string().splitlines(keepends=True)
865
+ from_file = f"local/{ctx.source_toml_path.name}"
866
+ to_file = f"upstream/{ctx.source_toml_path.name}"
859
867
 
860
868
  diff = difflib.unified_diff(
861
869
  from_lines,
@@ -863,16 +871,23 @@ def _print_diff(
863
871
  fromfile=from_file,
864
872
  tofile=to_file,
865
873
  )
866
- sys.stdout.writelines(diff)
874
+ fmt.diff("".join(diff))
867
875
 
868
876
 
869
- def _check_pre_commit_sync(args: Arguments) -> int | None:
877
+ def _check_pre_commit_sync(args: Arguments, fmt: ResultFormatter) -> int | None:
870
878
  """Return exit code 2 if pre-commit hook version is out of sync, otherwise None.
871
879
 
872
880
  Shared helper to avoid duplicating the pre-commit synchronization logic.
873
881
  """
874
882
  if getattr(args, "pre_commit", False) and not sync_pre_commit(pathlib.Path.cwd(), dry_run=True):
875
- print("⚠️ Pre-commit hook version is out of sync!")
883
+ repo_root = pathlib.Path.cwd()
884
+ pre_commit_config = repo_root / ".pre-commit-config.yaml"
885
+ file_path = pre_commit_config if pre_commit_config.exists() else repo_root
886
+ fmt.warning(
887
+ "⚠️ Pre-commit hook version is out of sync!",
888
+ logger=LOGGER,
889
+ file_path=file_path,
890
+ )
876
891
  return 2
877
892
  return None
878
893
 
@@ -898,13 +913,16 @@ async def check(
898
913
  ... )
899
914
  >>> # asyncio.run(check(args))
900
915
  """
901
- print("🔍 Checking Ruff sync status...")
916
+ fmt = get_formatter(args.output_format)
917
+ fmt.note("🔍 Checking Ruff sync status...")
902
918
 
903
919
  _source_toml_path = resolve_target_path(args.to, args.upstream).resolve(strict=False)
904
920
  if not _source_toml_path.exists():
905
- print(
921
+ fmt.error(
906
922
  f"❌ Configuration file {_source_toml_path} does not exist. "
907
- "Run 'ruff-sync pull' to create it."
923
+ "Run 'ruff-sync pull' to create it.",
924
+ file_path=_source_toml_path,
925
+ logger=LOGGER,
908
926
  )
909
927
  return 1
910
928
 
@@ -939,14 +957,14 @@ async def check(
939
957
  merged_val = merged_ruff.unwrap() if merged_ruff is not None else None
940
958
 
941
959
  if source_val == merged_val:
942
- print("✅ Ruff configuration is semantically in sync.")
943
- exit_code = _check_pre_commit_sync(args)
960
+ fmt.success("✅ Ruff configuration is semantically in sync.")
961
+ exit_code = _check_pre_commit_sync(args, fmt)
944
962
  if exit_code is not None:
945
963
  return exit_code
946
964
  return 0
947
965
  elif source_doc.as_string() == merged_doc.as_string():
948
- print("✅ Ruff configuration is in sync.")
949
- exit_code = _check_pre_commit_sync(args)
966
+ fmt.success("✅ Ruff configuration is in sync.")
967
+ exit_code = _check_pre_commit_sync(args, fmt)
950
968
  if exit_code is not None:
951
969
  return exit_code
952
970
  return 0
@@ -955,16 +973,23 @@ async def check(
955
973
  rel_path = _source_toml_path.relative_to(pathlib.Path.cwd())
956
974
  except ValueError:
957
975
  rel_path = _source_toml_path
958
- print(f"❌ Ruff configuration at {rel_path} is out of sync!")
976
+ fmt.error(
977
+ f"❌ Ruff configuration at {rel_path} is out of sync!",
978
+ file_path=rel_path,
979
+ logger=LOGGER,
980
+ )
959
981
 
960
982
  if args.diff:
961
983
  _print_diff(
962
984
  args=args,
963
- source_toml_path=_source_toml_path,
964
- source_doc=source_doc,
965
- merged_doc=merged_doc,
966
- source_val=source_val,
967
- merged_val=merged_val,
985
+ fmt=fmt,
986
+ ctx=DiffContext(
987
+ source_toml_path=_source_toml_path,
988
+ source_doc=source_doc,
989
+ merged_doc=merged_doc,
990
+ source_val=source_val,
991
+ merged_val=merged_val,
992
+ ),
968
993
  )
969
994
  return 1
970
995
 
@@ -1059,7 +1084,8 @@ async def pull(
1059
1084
  ... )
1060
1085
  >>> # asyncio.run(pull(args))
1061
1086
  """
1062
- print("🔄 Syncing Ruff...")
1087
+ fmt = get_formatter(args.output_format)
1088
+ fmt.note("🔄 Syncing Ruff...")
1063
1089
  _source_toml_path = resolve_target_path(args.to, args.upstream).resolve(strict=False)
1064
1090
 
1065
1091
  source_toml_file = TOMLFile(_source_toml_path)
@@ -1073,12 +1099,14 @@ async def pull(
1073
1099
  _source_toml_path.parent.mkdir(parents=True, exist_ok=True)
1074
1100
  _source_toml_path.touch()
1075
1101
  except OSError as e:
1076
- print(f"❌ Failed to create {_source_toml_path}: {e}", file=sys.stderr)
1102
+ fmt.error(f"❌ Failed to create {_source_toml_path}: {e}", logger=LOGGER)
1077
1103
  return 1
1078
1104
  else:
1079
- print(
1105
+ fmt.error(
1080
1106
  f"❌ Configuration file {_source_toml_path} does not exist. "
1081
- "Pass the '--init' flag to create it."
1107
+ "Pass the '--init' flag to create it.",
1108
+ file_path=_source_toml_path,
1109
+ logger=LOGGER,
1082
1110
  )
1083
1111
  return 1
1084
1112
 
@@ -1105,7 +1133,7 @@ async def pull(
1105
1133
  rel_path = _source_toml_path.resolve().relative_to(pathlib.Path.cwd())
1106
1134
  except ValueError:
1107
1135
  rel_path = _source_toml_path.resolve()
1108
- print(f"✅ Updated {rel_path}")
1136
+ fmt.success(f"✅ Updated {rel_path}")
1109
1137
 
1110
1138
  if args.pre_commit is not MISSING and args.pre_commit:
1111
1139
  sync_pre_commit(pathlib.Path.cwd(), dry_run=False)