ruff-sync 0.1.4.dev0__tar.gz → 0.1.4.dev1__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 (111) hide show
  1. ruff_sync-0.1.4.dev1/.agents/skills/ruff-sync-usage/references/ci-integration.md +213 -0
  2. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.pre-commit-config.yaml +1 -1
  3. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/PKG-INFO +1 -1
  4. ruff_sync-0.1.4.dev1/docs/ci-integration.md +170 -0
  5. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/pyproject.toml +1 -1
  6. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/cli.py +1 -1
  7. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/constants.py +1 -0
  8. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/core.py +113 -6
  9. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/formatters.py +245 -1
  10. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_basic.py +2 -2
  11. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_check.py +208 -2
  12. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_formatters.py +95 -0
  13. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/uv.lock +1 -1
  14. ruff_sync-0.1.4.dev0/.agents/skills/ruff-sync-usage/references/ci-integration.md +0 -134
  15. ruff_sync-0.1.4.dev0/docs/ci-integration.md +0 -103
  16. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/TESTING.md +0 -0
  17. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/formatters-architecture.md +0 -0
  18. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/gitlab-reports.md +0 -0
  19. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/issue-102-context.md +0 -0
  20. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/gh-issues/SKILL.md +0 -0
  21. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
  22. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/examples.md +0 -0
  23. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
  24. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
  25. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
  26. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
  27. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/release-notes-generation/SKILL.md +0 -0
  28. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
  29. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
  30. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
  31. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.agents/workflows/add-test-case.md +0 -0
  32. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.git-blame-ignore-revs +0 -0
  33. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.github/dependabot.yml +0 -0
  34. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.github/workflows/ci.yaml +0 -0
  35. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.github/workflows/complexity.yaml +0 -0
  36. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.github/workflows/docs.yaml +0 -0
  37. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.gitignore +0 -0
  38. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/.pre-commit-hooks.yaml +0 -0
  39. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/AGENTS.md +0 -0
  40. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/CONTRIBUTING.md +0 -0
  41. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/LICENSE.md +0 -0
  42. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/README.md +0 -0
  43. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/codecov.yml +0 -0
  44. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/configs/data-science-engineering/ruff.toml +0 -0
  45. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/configs/fastapi/ruff.toml +0 -0
  46. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/configs/kitchen-sink/ruff.toml +0 -0
  47. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/agent-skill.md +0 -0
  48. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/assets/favicon.png +0 -0
  49. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/assets/github-pr-annotation.png +0 -0
  50. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/assets/logo.png +0 -0
  51. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/assets/ruff_sync_banner.png +0 -0
  52. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/best-practices.md +0 -0
  53. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/configuration.md +0 -0
  54. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/contributing.md +0 -0
  55. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/examples/advanced-config.toml +0 -0
  56. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/examples/basic-config.toml +0 -0
  57. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/gen_ref_pages.py +0 -0
  58. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/index.md +0 -0
  59. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/installation.md +0 -0
  60. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/pre-commit.md +0 -0
  61. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/pre-defined-configs.md +0 -0
  62. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/troubleshooting.md +0 -0
  63. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/url-resolution.md +0 -0
  64. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/docs/usage.md +0 -0
  65. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/mkdocs.yml +0 -0
  66. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/scripts/check_dogfood.sh +0 -0
  67. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/scripts/gitclone_dogfood.sh +0 -0
  68. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/scripts/pull_dogfood.sh +0 -0
  69. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/skills-lock.json +0 -0
  70. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/__init__.py +0 -0
  71. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/__main__.py +0 -0
  72. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/src/ruff_sync/pre_commit.py +0 -0
  73. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tasks.py +0 -0
  74. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/__init__.py +0 -0
  75. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/conftest.py +0 -0
  76. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
  77. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
  78. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
  79. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
  80. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  81. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  82. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  83. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  84. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  85. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  86. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  87. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  88. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  89. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  90. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  91. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  92. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/standard_final.toml +0 -0
  93. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  94. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  95. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/ruff.toml +0 -0
  96. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_config_validation.py +0 -0
  97. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_constants.py +0 -0
  98. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_corner_cases.py +0 -0
  99. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_deprecation.py +0 -0
  100. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_e2e.py +0 -0
  101. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_git_fetch.py +0 -0
  102. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_pre_commit.py +0 -0
  103. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_project.py +0 -0
  104. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_scaffold.py +0 -0
  105. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_serialization.py +0 -0
  106. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_toml_operations.py +0 -0
  107. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_url_handling.py +0 -0
  108. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/test_whitespace.py +0 -0
  109. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  110. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  111. {ruff_sync-0.1.4.dev0 → ruff_sync-0.1.4.dev1}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -0,0 +1,213 @@
1
+ # CI Integration Recipes
2
+
3
+ ## GitHub Actions
4
+
5
+ ### Basic Drift Check
6
+
7
+ Add this step to any existing workflow (e.g., `.github/workflows/ci.yaml`):
8
+
9
+ ```yaml
10
+ - name: Check Ruff config is in sync
11
+ run: ruff-sync check --semantic --output-format github
12
+ ```
13
+
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.
16
+
17
+ ### Full Workflow Example
18
+
19
+ Uses [`astral-sh/setup-uv`](https://github.com/astral-sh/setup-uv) — the official action that installs uv, adds it to PATH, and handles caching. No separate `setup-python` step needed.
20
+
21
+ ```yaml
22
+ name: Ruff sync check
23
+
24
+ on:
25
+ push:
26
+ branches: [main]
27
+ pull_request:
28
+
29
+ jobs:
30
+ ruff-sync-check:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Install uv
36
+ uses: astral-sh/setup-uv@v6
37
+ with:
38
+ version: "0.10.x" # pin to a minor range; Dependabot can keep this current
39
+
40
+ - name: Install ruff-sync
41
+ run: uv tool install ruff-sync
42
+
43
+ - name: Check Ruff config is in sync with upstream
44
+ run: ruff-sync check --semantic --output-format github
45
+ ```
46
+
47
+ ### With Pre-commit Sync Check
48
+
49
+ To also verify the pre-commit hook version, add the `--pre-commit` flag:
50
+
51
+ ```yaml
52
+ - name: Check Ruff config and pre-commit hook
53
+ run: ruff-sync check --semantic --pre-commit --output-format github
54
+ ```
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.)
57
+
58
+ ### SARIF Upload (GitHub Advanced Security)
59
+
60
+ For repositories with GitHub Advanced Security enabled, upload SARIF results to track drift findings in the **Security tab** and get per-key inline PR annotations that persist across runs:
61
+
62
+ ```yaml
63
+ - name: Check Ruff config (SARIF)
64
+ run: ruff-sync check --output-format sarif > ruff-sync.sarif || true
65
+
66
+ - name: Upload SARIF results
67
+ uses: github/codeql-action/upload-sarif@v3
68
+ with:
69
+ sarif_file: ruff-sync.sarif
70
+ category: ruff-sync
71
+ ```
72
+
73
+ The `|| true` ensures the upload step always runs even when `ruff-sync` exits 1 (drift detected). Without it, GitHub Actions would skip the upload step on failure.
74
+
75
+ > **Why SARIF over `--output-format github`?**
76
+ > The `github` format creates ephemeral workflow annotations that disappear once the check re-runs. SARIF findings are persisted in the Security tab, tracked as "introduced" and "resolved" across branches, and each drifted TOML key (`lint.select`, `target-version`, etc.) is a separate finding with a stable fingerprint — making it easy to trend configuration health over time.
77
+
78
+ ---
79
+
80
+ ## GitLab CI
81
+
82
+ Use the official [`ghcr.io/astral-sh/uv`](https://docs.astral.sh/uv/guides/integration/gitlab/) image — uv is already on the `PATH`, no install step needed.
83
+
84
+ ```yaml
85
+ variables:
86
+ UV_VERSION: "0.10"
87
+ PYTHON_VERSION: "3.12"
88
+ BASE_LAYER: alpine
89
+ UV_LINK_MODE: copy # required: GitLab mounts build dir separately
90
+
91
+ ruff-sync-check:
92
+ stage: lint
93
+ image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
94
+ script:
95
+ - uvx ruff-sync check --semantic --output-format gitlab > gl-code-quality-report.json
96
+ artifacts:
97
+ when: always
98
+ reports:
99
+ codequality: gl-code-quality-report.json
100
+ paths:
101
+ - gl-code-quality-report.json
102
+ expire_in: 1 week
103
+ rules:
104
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
105
+ - if: '$CI_COMMIT_BRANCH == "main"'
106
+ ```
107
+
108
+ ### GitLab SAST Report / SARIF (Ultimate tier)
109
+
110
+ Use `--output-format sarif` to feed the GitLab [Security & Compliance dashboard](https://docs.gitlab.com/user/application_security/) via the `sast` artifact report type:
111
+
112
+ ```yaml
113
+ variables:
114
+ UV_VERSION: "0.10"
115
+ PYTHON_VERSION: "3.12"
116
+ BASE_LAYER: alpine
117
+ UV_LINK_MODE: copy # required: GitLab mounts build dir separately
118
+
119
+ ruff-sync-sarif:
120
+ stage: lint
121
+ image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
122
+ script:
123
+ - uvx ruff-sync check --output-format sarif > ruff-sync.sarif
124
+ artifacts:
125
+ when: always # Upload even when ruff-sync exits 1 (drift detected)
126
+ reports:
127
+ sast: ruff-sync.sarif
128
+ paths:
129
+ - ruff-sync.sarif
130
+ expire_in: 1 week
131
+ rules:
132
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
133
+ ```
134
+
135
+ > **Why SARIF over `--output-format gitlab` (codequality)?**
136
+ >
137
+ > | Concern | `codequality` | `sarif` |
138
+ > |---------|--------------|--------|
139
+ > | GitLab tier | Free (MR widget), Ultimate (inline diff) | Ultimate (Security dashboard) |
140
+ > | GitHub support | ❌ | ✅ via `upload-sarif` |
141
+ > | Per-key findings | ❌ one issue per file | ✅ one finding per drifted TOML key |
142
+ > | Finding persistence | MR widget only | Security tab, tracked across branches |
143
+ > | Portability | GitLab only | GitHub, GitLab, SonarQube, IDE extensions |
144
+ >
145
+ > **Rule of thumb**: use `codequality` for lightweight GitLab-native linting feedback; use `sarif` when you need cross-platform compatibility or want findings tracked in a security/code-scanning dashboard.
146
+
147
+ ---
148
+
149
+ ## Pre-commit Hook
150
+
151
+ Run `ruff-sync check` as a pre-commit hook to catch drift before every commit:
152
+
153
+ ```yaml
154
+ # .pre-commit-config.yaml
155
+ - repo: https://github.com/Kilo59/ruff-sync
156
+ rev: v0.1.3 # pin to a release tag
157
+ hooks:
158
+ - id: ruff-sync-check
159
+ ```
160
+
161
+ The hook runs `ruff-sync check --semantic` automatically. Update `rev` to the latest ruff-sync version.
162
+
163
+ ---
164
+
165
+ ## Makefile
166
+
167
+ ```makefile
168
+ .PHONY: sync-check sync
169
+
170
+ sync-check:
171
+ ruff-sync check --semantic
172
+
173
+ sync:
174
+ ruff-sync
175
+ git diff pyproject.toml
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Deciding: `--semantic` vs. Full String Check
181
+
182
+ | Mode | Fails on | Use when |
183
+ |------|---------|---------|
184
+ | `ruff-sync check --semantic` | Value/rule differences only | CI — avoids false positives from local comment edits |
185
+ | `ruff-sync check` | Any string difference (comments, whitespace, values) | Enforcing exact config file consistency |
186
+
187
+ Recommendation: **use `--semantic` in CI** and save the full-string check for auditing purposes.
188
+
189
+ ---
190
+
191
+ ## Dogfooding (Self-Check)
192
+
193
+ If `ruff-sync` is configured in the project's own `pyproject.toml` (the standard case), just run:
194
+
195
+ ```bash
196
+ ruff-sync check
197
+ ```
198
+
199
+ No URL argument needed — it reads `upstream` from `[tool.ruff-sync]`.
200
+
201
+ ---
202
+
203
+ ## Exit Codes
204
+
205
+ | Code | Meaning |
206
+ |------|----------|
207
+ | **0** | In sync — no drift detected |
208
+ | **1** | Config drift — `[tool.ruff]` values differ from upstream |
209
+ | **2** | CLI usage error — invalid arguments (reserved by argparse) |
210
+ | **3** | Pre-commit hook drift — use `--pre-commit` flag to enable this check |
211
+ | **4** | Upstream unreachable — HTTP error or network failure |
212
+
213
+ All non-zero codes cause a CI step to fail, which is the desired behaviour. To diagnose which failure occurred, check the exit code with `echo $?` after the `ruff-sync check` call.
@@ -28,7 +28,7 @@ repos:
28
28
  types_or: [yaml, json]
29
29
  exclude: .agents/skills/mkdocs-generation/templates/mkdocs.yml
30
30
  - repo: https://github.com/rhysd/actionlint
31
- rev: v1.7.11
31
+ rev: v1.7.12
32
32
  hooks:
33
33
  - id: actionlint
34
34
  exclude: .github/workflows/complexity.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruff-sync
3
- Version: 0.1.4.dev0
3
+ Version: 0.1.4.dev1
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/
@@ -0,0 +1,170 @@
1
+ # CI Integration
2
+
3
+ `ruff-sync` is designed to be run in CI pipelines to ensure that all repositories in an organization stay in sync with the central standards.
4
+
5
+ ## Usage in CI
6
+
7
+ The best way to use `ruff-sync` in CI is with the `check` command. If the configuration has drifted, `ruff-sync check` will exit with a non-zero code, failing the build.
8
+
9
+ ### GitHub Actions
10
+
11
+ We recommend using `uv` to run `ruff-sync` in GitHub Actions.
12
+
13
+ #### Basic Check
14
+
15
+ ```yaml
16
+ name: "Standards Check"
17
+
18
+ on:
19
+ pull_request:
20
+ branches: [main]
21
+
22
+ jobs:
23
+ ruff-sync:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ - name: Check Ruff Config
29
+ run: uvx ruff-sync check --semantic --output-format github
30
+ ```
31
+
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
+ ![GitHub PR Annotation](assets/github-pr-annotation.png)
35
+
36
+ ### SARIF Upload (GitHub Advanced Security)
37
+
38
+ For repositories with [GitHub Advanced Security](https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security) enabled, upload SARIF results to track drift findings in the **Security tab** and get per-key inline PR annotations:
39
+
40
+ ```yaml
41
+ - name: Check Ruff config (SARIF)
42
+ run: ruff-sync check --output-format sarif > ruff-sync.sarif || true
43
+
44
+ - name: Upload SARIF results
45
+ uses: github/codeql-action/upload-sarif@v3
46
+ with:
47
+ sarif_file: ruff-sync.sarif
48
+ category: ruff-sync
49
+ ```
50
+
51
+ The `|| true` ensures the upload step always runs even when drift is detected (exit code 1).
52
+
53
+ > **Why SARIF over `--output-format github`?** The `github` format creates ephemeral PR annotations that disappear after the check re-runs. SARIF findings are persisted in the Security tab, tracked as "introduced" and "resolved" across branches, and each drifted TOML key (`lint.select`, `target-version`, etc.) appears as a separate, deduplicated finding.
54
+
55
+ ### Automated Sync PRs
56
+
57
+ Instead of just checking, you can have a bot automatically open a PR when the upstream configuration changes.
58
+
59
+ ```yaml
60
+ name: "Upstream Sync"
61
+
62
+ on:
63
+ schedule:
64
+ - cron: '0 0 * * 1' # Every Monday at midnight
65
+ workflow_dispatch:
66
+
67
+ jobs:
68
+ sync:
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: astral-sh/setup-uv@v5
73
+ - name: Pull upstream
74
+ run: uvx ruff-sync
75
+ - name: Create Pull Request
76
+ uses: peter-evans/create-pull-request@v6
77
+ with:
78
+ commit-message: "chore: sync ruff configuration from upstream"
79
+ title: "chore: sync ruff configuration"
80
+ body: "This PR synchronizes the Ruff configuration with the upstream source."
81
+ branch: "ruff-sync-update"
82
+ ```
83
+
84
+ ### GitLab CI
85
+
86
+ #### Code Quality Report (Free tier)
87
+
88
+ Use `--output-format gitlab` to produce a [GitLab Code Quality](https://docs.gitlab.com/ci/testing/code_quality/) report. This appears in the MR widget on the Free tier and as inline diff annotations on Ultimate.
89
+
90
+ ```yaml
91
+ ruff-sync-check:
92
+ stage: lint
93
+ image: ghcr.io/astral-sh/uv:latest
94
+ script:
95
+ - uvx ruff-sync check --semantic --output-format gitlab > gl-code-quality-report.json
96
+ artifacts:
97
+ when: always # Upload even when ruff-sync exits 1 (drift detected)
98
+ reports:
99
+ codequality: gl-code-quality-report.json
100
+ paths:
101
+ - gl-code-quality-report.json # Also expose for download/browsing
102
+ expire_in: 1 week
103
+ rules:
104
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
105
+ - if: '$CI_COMMIT_BRANCH == "main"'
106
+ ```
107
+
108
+ #### SAST Report / SARIF (Ultimate tier)
109
+
110
+ Use `--output-format sarif` to feed the GitLab [Security & Compliance dashboard](https://docs.gitlab.com/user/application_security/) via the `sast` artifact report type:
111
+
112
+ ```yaml
113
+ ruff-sync-sarif:
114
+ stage: lint
115
+ image: ghcr.io/astral-sh/uv:latest
116
+ script:
117
+ - uvx ruff-sync check --output-format sarif > ruff-sync.sarif
118
+ artifacts:
119
+ when: always
120
+ reports:
121
+ sast: ruff-sync.sarif
122
+ paths:
123
+ - ruff-sync.sarif
124
+ expire_in: 1 week
125
+ rules:
126
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
127
+ ```
128
+
129
+ > **Why SARIF over `codequality`?** SARIF is a portable, vendor-neutral format — the same file works for GitHub Advanced Security, GitLab SAST, SonarQube, and IDE extensions. It also carries per-key granularity: each drifted TOML key is a separate finding with a stable fingerprint that is tracked as "introduced" or "resolved" across pipeline runs. Use `codequality` for lightweight GitLab-native linting feedback; use `sarif` when you need cross-platform compatibility or want findings tracked in a security dashboard.
130
+
131
+ ---
132
+
133
+ You can use `ruff-sync` with `pre-commit` to ensure your configuration is always in sync before pushing.
134
+
135
+ See the [Pre-commit Guide](pre-commit.md) for details on using the official hooks.
136
+
137
+ ---
138
+
139
+ ## Exit Codes
140
+
141
+ | Code | Meaning |
142
+ |------|----------|
143
+ | **0** | In sync — no drift detected |
144
+ | **1** | Config drift — `[tool.ruff]` values differ from upstream |
145
+ | **2** | CLI usage error — invalid arguments (reserved by argparse) |
146
+ | **3** | Pre-commit hook drift — use `--pre-commit` flag to enable this check |
147
+ | **4** | Upstream unreachable — HTTP error or network failure |
148
+
149
+ All non-zero codes cause a CI step to fail. Use `artifacts: when: always` (GitLab) or `if: always()` (GitHub Actions) to ensure report artifacts are uploaded even when the job fails.
150
+
151
+ ---
152
+
153
+ ## 💡 Best Practices
154
+
155
+ > [!TIP]
156
+ > Read the complete [Best Practices](best-practices.md) guide for a broader look at organizing `ruff-sync` deployments, including when semantic checks should be blocking vs. informational.
157
+
158
+ ### Use `--semantic`
159
+
160
+ In CI, you usually only care about the functional configuration. Using `--semantic` ensures that minor formatting changes don't break your builds, while still guaranteeing that the actual rules are identical.
161
+
162
+ ### Handle Exclusions Properly
163
+
164
+ If your project intentionally diverges from the upstream (e.g., using different `per-file-ignores` or ignoring a specific rule), ensure those overrides are listed in the `[tool.ruff-sync]` `exclude` list in your `pyproject.toml`.
165
+
166
+ The `check` command respects your local `exclude` list. If you exclude a setting, `ruff-sync check` will completely ignore it when comparing against the upstream, ensuring that intended deviations never cause CI to fail!
167
+
168
+ ### Use a Dedicated Workflow
169
+
170
+ Running `ruff-sync` as a separate job in your linting workflow makes it easy to identify when a failure is due to configuration drift rather than a code quality issue.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.1.4.dev0"
3
+ version = "0.1.4.dev1"
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 = [
@@ -506,7 +506,7 @@ def main() -> int:
506
506
  except UpstreamError as e:
507
507
  for url, err in e.errors:
508
508
  LOGGER.error(f"❌ Failed to fetch {url}: {err}") # noqa: TRY400
509
- return 1
509
+ return 4
510
510
 
511
511
 
512
512
  if __name__ == "__main__":
@@ -53,6 +53,7 @@ class OutputFormat(str, enum.Enum):
53
53
  JSON = "json"
54
54
  GITHUB = "github"
55
55
  GITLAB = "gitlab"
56
+ SARIF = "sarif"
56
57
 
57
58
  @override
58
59
  def __str__(self) -> str:
@@ -875,9 +875,11 @@ def _print_diff(
875
875
 
876
876
 
877
877
  def _check_pre_commit_sync(args: Arguments, fmt: ResultFormatter) -> int | None:
878
- """Return exit code 2 if pre-commit hook version is out of sync, otherwise None.
878
+ """Return exit code 3 if pre-commit hook version is out of sync, otherwise None.
879
879
 
880
880
  Shared helper to avoid duplicating the pre-commit synchronization logic.
881
+ Exit code 3 is reserved for pre-commit hook drift to avoid collision with
882
+ argparse (2) and config drift (1).
881
883
  """
882
884
  if getattr(args, "pre_commit", False) and not sync_pre_commit(pathlib.Path.cwd(), dry_run=True):
883
885
  repo_root = pathlib.Path.cwd()
@@ -888,10 +890,112 @@ def _check_pre_commit_sync(args: Arguments, fmt: ResultFormatter) -> int | None:
888
890
  logger=LOGGER,
889
891
  file_path=file_path,
890
892
  )
891
- return 2
893
+ return 3
892
894
  return None
893
895
 
894
896
 
897
+ def _find_changed_keys(
898
+ source: Any,
899
+ merged: Any,
900
+ prefix: str = "",
901
+ ) -> list[str]:
902
+ """Return a list of dotted TOML keys that differ between *source* and *merged*.
903
+
904
+ Recursively walks both tables and returns keys whose leaf values have
905
+ changed or that are present only in *merged* (added by upstream). Keys
906
+ that exist only in *source* (local-only additions) are intentionally
907
+ excluded — ruff-sync never removes local keys.
908
+
909
+ Args:
910
+ source: The original (local) TOML table or value.
911
+ merged: The merged (upstream-applied) TOML table or value.
912
+ prefix: Dotted key prefix built up during recursion.
913
+
914
+ Returns:
915
+ A list of dotted key paths, e.g. ``["lint.select", "target-version"]``.
916
+ """
917
+ changed: list[str] = []
918
+
919
+ source_is_mapping = isinstance(source, Mapping)
920
+ merged_is_mapping = isinstance(merged, Mapping)
921
+
922
+ if source_is_mapping and merged_is_mapping:
923
+ for key, merged_val in merged.items():
924
+ full_key = f"{prefix}.{key}" if prefix else key
925
+ source_val = source.get(key)
926
+ if source_val is None and key not in source:
927
+ # Key is new (only in merged)
928
+ changed.append(full_key)
929
+ else:
930
+ # Unwrap tomlkit proxy objects before comparing.
931
+ # Note: source_val is always set here because the key exists in source
932
+ # (the is None + key not in guard above handles the absent case).
933
+ src_unwrapped = (
934
+ source_val.unwrap()
935
+ if source_val is not None and hasattr(source_val, "unwrap")
936
+ else source_val
937
+ )
938
+ mrg_unwrapped = merged_val.unwrap() if hasattr(merged_val, "unwrap") else merged_val
939
+ nested = _find_changed_keys(src_unwrapped, mrg_unwrapped, prefix=full_key)
940
+ if nested:
941
+ changed.extend(nested)
942
+ elif src_unwrapped != mrg_unwrapped:
943
+ changed.append(full_key)
944
+ elif source_is_mapping != merged_is_mapping:
945
+ # Structural type mismatch (table vs scalar or vice-versa): treat the
946
+ # whole node as changed rather than attempting a meaningless value
947
+ # comparison between incompatible TOML node types.
948
+ changed.append(prefix or ".")
949
+ else:
950
+ # Both are leaf values — compare directly.
951
+ src_unwrapped = source.unwrap() if hasattr(source, "unwrap") else source
952
+ mrg_unwrapped = merged.unwrap() if hasattr(merged, "unwrap") else merged
953
+ if src_unwrapped != mrg_unwrapped:
954
+ changed.append(prefix or ".")
955
+
956
+ return changed
957
+
958
+
959
+ def _report_drift(
960
+ fmt: ResultFormatter,
961
+ rel_path: pathlib.Path,
962
+ source_doc: TOMLDocument,
963
+ merged_doc: TOMLDocument,
964
+ is_ruff_toml: bool,
965
+ ) -> None:
966
+ """Report configuration drift by emitting one error per changed key.
967
+
968
+ Emits one ``fmt.error()`` call per differing dotted key so that structured
969
+ formatters (SARIF, GitLab, JSON) can build per-key fingerprints. If no
970
+ granular keys are found (whitespace-only diff), falls back to a single
971
+ generic error.
972
+ """
973
+ if is_ruff_toml:
974
+ source_section: Any = source_doc
975
+ merged_section: Any = merged_doc
976
+ else:
977
+ source_section = source_doc.get("tool", {}).get("ruff") or {}
978
+ merged_section = merged_doc.get("tool", {}).get("ruff") or {}
979
+ changed_keys = sorted(set(_find_changed_keys(source_section, merged_section)))
980
+
981
+ if changed_keys:
982
+ for drift_key in changed_keys:
983
+ fmt.error(
984
+ f"\u274c Key '{drift_key}' in {rel_path} is out of sync! "
985
+ "Run `ruff-sync` to update.",
986
+ file_path=rel_path,
987
+ logger=LOGGER,
988
+ drift_key=drift_key,
989
+ )
990
+ else:
991
+ # Structural change only (e.g. whitespace) — single generic error.
992
+ fmt.error(
993
+ f"\u274c Ruff configuration at {rel_path} is out of sync! Run `ruff-sync` to update.",
994
+ file_path=rel_path,
995
+ logger=LOGGER,
996
+ )
997
+
998
+
895
999
  async def check(
896
1000
  args: Arguments,
897
1001
  ) -> int:
@@ -974,10 +1078,13 @@ async def check(
974
1078
  rel_path = _source_toml_path.relative_to(pathlib.Path.cwd())
975
1079
  except ValueError:
976
1080
  rel_path = _source_toml_path
977
- fmt.error(
978
- f"❌ Ruff configuration at {rel_path} is out of sync! Run `ruff-sync` to update.",
979
- file_path=rel_path,
980
- logger=LOGGER,
1081
+
1082
+ _report_drift(
1083
+ fmt=fmt,
1084
+ rel_path=rel_path,
1085
+ source_doc=source_doc,
1086
+ merged_doc=merged_doc,
1087
+ is_ruff_toml=is_ruff_toml_file(_source_toml_path.name),
981
1088
  )
982
1089
 
983
1090
  if args.diff: