ruff-sync 0.1.5.dev4__tar.gz → 0.1.5.dev5__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 (176) hide show
  1. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/ruff-sync-usage/references/ci-integration.md +11 -2
  2. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/PKG-INFO +1 -1
  3. ruff_sync-0.1.5.dev5/docs/assets/github-job-summary.png +0 -0
  4. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/ci-integration.md +10 -1
  5. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/usage.md +5 -3
  6. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/pyproject.toml +1 -1
  7. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/formatters.py +111 -20
  8. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_formatters.py +140 -31
  9. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/uv.lock +1 -1
  10. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/DEPENDENCIES.md +0 -0
  11. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/TESTING.md +0 -0
  12. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/decisions/0001-type-refactoring-strategy.md +0 -0
  13. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/decisions/0002-tui-node-ast.md +0 -0
  14. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/decisions/README.md +0 -0
  15. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/formatters-architecture.md +0 -0
  16. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/gitlab-reports.md +0 -0
  17. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/issue-102-context.md +0 -0
  18. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/ruff.toml +0 -0
  19. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/adr/SKILL.md +0 -0
  20. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/dirty-equals/SKILL.md +0 -0
  21. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/dirty-equals/references/common-matchers.md +0 -0
  22. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/dirty-equals/references/toml-matching.md +0 -0
  23. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/dirty-equals/trigger_eval.json +0 -0
  24. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/gh-issues/SKILL.md +0 -0
  25. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mike/SKILL.md +0 -0
  26. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mike/references/commands.md +0 -0
  27. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
  28. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/examples.md +0 -0
  29. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
  30. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
  31. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
  32. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
  33. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/release-notes-generation/SKILL.md +0 -0
  34. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
  35. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
  36. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
  37. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/SKILL.md +0 -0
  38. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/best-practices.md +0 -0
  39. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/evaluating-skills.md +0 -0
  40. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/optimizing-descriptions.md +0 -0
  41. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/quickstart.md +0 -0
  42. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/sources.md +0 -0
  43. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/references/using-scripts.md +0 -0
  44. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/skill-creator/scripts/scaffold_skill.py +0 -0
  45. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/SKILL.md +0 -0
  46. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/examples/basic_app.py +0 -0
  47. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/examples/reactive_example.py +0 -0
  48. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/references/events.md +0 -0
  49. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/references/styling.md +0 -0
  50. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/references/testing.md +0 -0
  51. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/textual/references/widgets.md +0 -0
  52. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/SKILL.md +0 -0
  53. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/evals.json +0 -0
  54. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/advanced-narrowing.md +0 -0
  55. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/error-code-lookup.md +0 -0
  56. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/generics.md +0 -0
  57. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/naming.md +0 -0
  58. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/protocol-patterns.md +0 -0
  59. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/quickstart.md +0 -0
  60. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/references/refactoring-patterns.md +0 -0
  61. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/skills/type-checking/scripts/audit_types.py +0 -0
  62. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/tui_design.md +0 -0
  63. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/tui_requirements.md +0 -0
  64. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/tui_rule_browsing.md +0 -0
  65. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/tui_rule_browsing_design.md +0 -0
  66. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.agents/workflows/add-test-case.md +0 -0
  67. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.git-blame-ignore-revs +0 -0
  68. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.github/dependabot.yml +0 -0
  69. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.github/workflows/ci.yaml +0 -0
  70. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.github/workflows/complexity.yaml +0 -0
  71. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.github/workflows/docs.yaml +0 -0
  72. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.gitignore +0 -0
  73. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.pre-commit-config.yaml +0 -0
  74. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/.pre-commit-hooks.yaml +0 -0
  75. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/AGENTS.md +0 -0
  76. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/CONTRIBUTING.md +0 -0
  77. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/LICENSE.md +0 -0
  78. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/README.md +0 -0
  79. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/codecov.yml +0 -0
  80. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/configs/data-science-engineering/ruff.toml +0 -0
  81. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/configs/fastapi/ruff.toml +0 -0
  82. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/configs/kitchen-sink/ruff.toml +0 -0
  83. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/agent-skill.md +0 -0
  84. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/assets/favicon.png +0 -0
  85. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/assets/github-pr-annotation.png +0 -0
  86. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/assets/logo.png +0 -0
  87. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/assets/ruff_sync_banner.png +0 -0
  88. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/best-practices.md +0 -0
  89. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/configuration.md +0 -0
  90. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/contributing.md +0 -0
  91. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/examples/advanced-config.toml +0 -0
  92. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/examples/basic-config.toml +0 -0
  93. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/gen_ref_pages.py +0 -0
  94. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/index.md +0 -0
  95. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/installation.md +0 -0
  96. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/overrides/main.html +0 -0
  97. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/overrides/partials/version_warning.html +0 -0
  98. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/pre-commit.md +0 -0
  99. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/pre-defined-configs.md +0 -0
  100. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/troubleshooting.md +0 -0
  101. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/docs/url-resolution.md +0 -0
  102. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/mkdocs.yml +0 -0
  103. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/scripts/check_dogfood.sh +0 -0
  104. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/scripts/gitclone_dogfood.sh +0 -0
  105. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/scripts/pull_dogfood.sh +0 -0
  106. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/skills-lock.json +0 -0
  107. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/__init__.py +0 -0
  108. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/__main__.py +0 -0
  109. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/cli.py +0 -0
  110. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/config_io.py +0 -0
  111. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/constants.py +0 -0
  112. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/core.py +0 -0
  113. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/dependencies.py +0 -0
  114. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/pre_commit.py +0 -0
  115. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/system.py +0 -0
  116. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/__init__.py +0 -0
  117. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/app.py +0 -0
  118. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/constants.py +0 -0
  119. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/screens.py +0 -0
  120. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/themes.py +0 -0
  121. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/types_.py +0 -0
  122. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/tui/widgets.py +0 -0
  123. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/src/ruff_sync/types_.py +0 -0
  124. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tasks.py +0 -0
  125. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/__init__.py +0 -0
  126. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/conftest.py +0 -0
  127. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
  128. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
  129. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
  130. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
  131. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  132. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  133. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  134. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  135. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  136. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  137. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  138. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  139. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  140. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  141. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  142. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  143. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/standard_final.toml +0 -0
  144. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  145. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  146. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/ruff.toml +0 -0
  147. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_basic.py +0 -0
  148. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_check.py +0 -0
  149. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_ci_integration.py +0 -0
  150. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_ci_validation.py +0 -0
  151. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_config_io.py +0 -0
  152. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_config_validation.py +0 -0
  153. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_constants.py +0 -0
  154. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_corner_cases.py +0 -0
  155. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_dependencies.py +0 -0
  156. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_deprecation.py +0 -0
  157. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_e2e.py +0 -0
  158. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_git_fetch.py +0 -0
  159. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_minimal_imports.sh +0 -0
  160. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_pre_commit.py +0 -0
  161. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_project.py +0 -0
  162. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_rule_logic.py +0 -0
  163. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_scaffold.py +0 -0
  164. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_serialization.py +0 -0
  165. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_system.py +0 -0
  166. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_toml_operations.py +0 -0
  167. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_url_handling.py +0 -0
  168. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/test_whitespace.py +0 -0
  169. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/tui/__init__.py +0 -0
  170. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/tui/conftest.py +0 -0
  171. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/tui/test_themes.py +0 -0
  172. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/tui/test_tui.py +0 -0
  173. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/tui/test_tui_types.py +0 -0
  174. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  175. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  176. {ruff_sync-0.1.5.dev4 → ruff_sync-0.1.5.dev5}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -12,7 +12,10 @@ Add this step to any existing workflow (e.g., `.github/workflows/ci.yaml`):
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
+ `--output-format github` creates inline PR annotations and a structured Job Summary report.
16
+
17
+ > [!TIP]
18
+ > `ruff-sync` automatically groups multiple drifts in the same file into a single annotation to reduce PR noise.
16
19
 
17
20
  ### Full Workflow Example
18
21
 
@@ -73,7 +76,13 @@ For repositories with GitHub Advanced Security enabled, upload SARIF results to
73
76
  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
77
 
75
78
  > **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.
79
+ >
80
+ > | Feature | `github` | `sarif` |
81
+ > |---------|----------|---------|
82
+ > | **PR Feedback** | Inline annotations (grouped) | Inline annotations (per-key) |
83
+ > | **Job Summary** | ✅ Markdown table | ❌ (requires separate parsing) |
84
+ > | **Persistence** | Ephemeral (until re-run) | Persistent (Security tab) |
85
+ > | **Tracking** | Manual | Automated "introduced/resolved" |
77
86
 
78
87
  ---
79
88
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruff-sync
3
- Version: 0.1.5.dev4
3
+ Version: 0.1.5.dev5
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/
@@ -31,8 +31,17 @@ jobs:
31
31
 
32
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
33
 
34
+ > [!NOTE]
35
+ > If multiple configuration drifts are detected in the same file, `ruff-sync` automatically groups them into a single, combined annotation to reduce visual noise and preserve your GitHub Actions annotation limits.
36
+
34
37
  ![GitHub PR Annotation](assets/github-pr-annotation.png)
35
38
 
39
+ #### Job Summary
40
+
41
+ In addition to inline annotations, `ruff-sync` writes a structured Markdown report to the GitHub Actions **Job Summary** page. This provides a high-level overview of all drifts detected across the entire run, making it easy to see exactly what needs to be synchronized without scrolling through log files.
42
+
43
+ ![GitHub Job Summary](assets/github-job-summary.png)
44
+
36
45
  ### SARIF Upload (GitHub Advanced Security)
37
46
 
38
47
  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:
@@ -50,7 +59,7 @@ For repositories with [GitHub Advanced Security](https://docs.github.com/en/get-
50
59
 
51
60
  The `|| true` ensures the upload step always runs even when drift is detected (exit code 1).
52
61
 
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.
62
+ > **Why SARIF over `--output-format github`?** The `github` format is excellent for lightweight, immediate feedback with a summarized job report. However, 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 in the GitHub Security dashboard.
54
63
 
55
64
  ### Automated Sync PRs
56
65
 
@@ -165,10 +165,12 @@ 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.
168
+ * **`--output-format [text|json|github|gitlab|sarif]`**: Specify the output format for synchronization results.
169
169
  - `text` (default): Human-readable terminal output.
170
170
  - `json`: Machine-readable JSON output for tool integration.
171
- - `github`: GitHub Actions workflow commands (`::error`, `::warning`) for inline PR annotations.
171
+ - `github`: GitHub Actions workflow commands and Job Summary report.
172
+ - `gitlab`: GitLab Code Quality report (JSON format).
173
+ - `sarif`: Static Analysis Results Interchange Format (SARIF) for GitHub Security or GitLab SAST.
172
174
  * **`--pre-commit`**: Sync the `astral-sh/ruff-pre-commit` hook version inside `.pre-commit-config.yaml` with the project's Ruff version.
173
175
 
174
176
  ### `check`
@@ -186,7 +188,7 @@ ruff-sync check [UPSTREAM_URL...] [--semantic] [--diff] [--pre-commit]
186
188
  * **`UPSTREAM_URL...`**: The source URL(s). Optional if defined locally.
187
189
  * **`--semantic`**: Ignore "non-functional" differences like whitespace, comments, or key order. Only errors if the actual Python-level data differs.
188
190
  * **`--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`.
191
+ * **`--output-format [text|json|github|gitlab|sarif]`**: Specify the output format for check results (defaults to `text`).
190
192
  * **`--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.
191
193
 
192
194
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.1.5.dev4"
3
+ version = "0.1.5.dev5"
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 = [
@@ -5,9 +5,11 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import pathlib
9
10
  from typing import Any, Final, Literal, Protocol, TypedDict
10
11
 
12
+ from ruff_sync.config_io import RuffConfigFileName
11
13
  from ruff_sync.constants import OutputFormat
12
14
 
13
15
  LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
@@ -150,13 +152,29 @@ class TextFormatter:
150
152
  """No-op for streaming formatters."""
151
153
 
152
154
 
155
+ class GithubIssue(TypedDict):
156
+ """GitHub Action output issue."""
157
+
158
+ level: Literal["error", "warning"]
159
+ message: str
160
+ file_path: pathlib.Path | None
161
+ drift_key: str | None
162
+
163
+
153
164
  class GithubFormatter:
154
165
  """GitHub Actions output formatter.
155
166
 
156
167
  Emits ``::error::`` and ``::warning::`` workflow commands for inline
157
- annotations.
168
+ annotations, and writes a Markdown report to ``GITHUB_STEP_SUMMARY``.
158
169
  """
159
170
 
171
+ _DEFAULT_PATH: Final[pathlib.Path] = pathlib.Path(RuffConfigFileName.PYPROJECT_TOML)
172
+
173
+ def __init__(self) -> None:
174
+ """Initialise an empty issue list."""
175
+ self._errors: list[GithubIssue] = []
176
+ self._warnings: list[GithubIssue] = []
177
+
160
178
  @staticmethod
161
179
  def _escape(value: str, is_property: bool = False) -> str:
162
180
  r"""Escapes characters for GitHub Actions workflow commands.
@@ -170,6 +188,11 @@ class GithubFormatter:
170
188
  return escaped.replace(":", "%3A").replace(",", "%2C")
171
189
  return escaped
172
190
 
191
+ @staticmethod
192
+ def _strip_status_prefix(message: str) -> str:
193
+ """Return the message without leading status emoji (e.g. ❌, ⚠️)."""
194
+ return message.removeprefix("❌ ").removeprefix("⚠️ ")
195
+
173
196
  def note(self, message: str) -> None:
174
197
  """Print a status note (standard stdout)."""
175
198
  print(message)
@@ -190,17 +213,17 @@ class GithubFormatter:
190
213
  check_name: str = _DEFAULT_CHECK_NAME,
191
214
  drift_key: str | None = None,
192
215
  ) -> None:
193
- """Print an error message as a GitHub Action error annotation."""
216
+ """Accumulate an error-level finding."""
194
217
  # Delegate standard log output to the logger to preserve context
195
218
  (logger or LOGGER).error(message)
196
-
197
- file_val = self._escape(str(file_path), is_property=True) if file_path else ""
198
- file_arg = f"file={file_val},line=1," if file_path else ""
199
- title_val = self._escape("Ruff Sync Error", is_property=True)
200
-
201
- clean_msg = message.removeprefix("").removeprefix("⚠️ ")
202
- escaped_msg = self._escape(clean_msg)
203
- print(f"::error {file_arg}title={title_val}::{escaped_msg}")
219
+ self._errors.append(
220
+ {
221
+ "level": "error",
222
+ "message": message,
223
+ "file_path": file_path,
224
+ "drift_key": drift_key,
225
+ }
226
+ )
204
227
 
205
228
  def warning(
206
229
  self,
@@ -210,16 +233,16 @@ class GithubFormatter:
210
233
  check_name: str = _DEFAULT_CHECK_NAME,
211
234
  drift_key: str | None = None,
212
235
  ) -> None:
213
- """Print a warning message as a GitHub Action warning annotation."""
236
+ """Accumulate a warning-level finding."""
214
237
  (logger or LOGGER).warning(message)
215
-
216
- file_val = self._escape(str(file_path), is_property=True) if file_path else ""
217
- file_arg = f"file={file_val},line=1," if file_path else ""
218
- title_val = self._escape("Ruff Sync Warning", is_property=True)
219
-
220
- clean_msg = message.removeprefix("").removeprefix("⚠️ ")
221
- escaped_msg = self._escape(clean_msg)
222
- print(f"::warning {file_arg}title={title_val}::{escaped_msg}")
238
+ self._warnings.append(
239
+ {
240
+ "level": "warning",
241
+ "message": message,
242
+ "file_path": file_path,
243
+ "drift_key": drift_key,
244
+ }
245
+ )
223
246
 
224
247
  def debug(self, message: str, logger: logging.Logger | None = None) -> None:
225
248
  """Print a debug message as a GitHub Action debug annotation."""
@@ -232,7 +255,75 @@ class GithubFormatter:
232
255
  print(diff_text, end="")
233
256
 
234
257
  def finalize(self) -> None:
235
- """No-op for streaming formatters."""
258
+ """Finalize and emit all accumulated issues as GitHub workflow commands.
259
+
260
+ If multiple issues exist for a single file/level, they are combined into
261
+ a single multi-line annotation to reduce visual noise.
262
+
263
+ A Markdown summary is also written to ``GITHUB_STEP_SUMMARY`` if the
264
+ environment variable is set.
265
+ """
266
+ all_issues = self._errors + self._warnings
267
+
268
+ # 1. Emit Inline Annotations
269
+ self._emit_annotations()
270
+
271
+ # 2. Write Step Summary
272
+ self._write_summary(all_issues)
273
+
274
+ def _emit_annotations(self) -> None:
275
+ """Group and emit issues as GitHub Action workflow commands."""
276
+ for level, issues in [("error", self._errors), ("warning", self._warnings)]:
277
+ # Group by file path object
278
+ by_file: dict[pathlib.Path, list[GithubIssue]] = {}
279
+ for issue in issues:
280
+ file_key = issue["file_path"] or self._DEFAULT_PATH
281
+ by_file.setdefault(file_key, []).append(issue)
282
+
283
+ for file_path, file_issues in by_file.items():
284
+ if not file_issues:
285
+ continue
286
+
287
+ title_prefix = "Error" if level == "error" else "Warning"
288
+ title_val = self._escape(f"Ruff Sync {title_prefix}", is_property=True)
289
+ file_val = self._escape(str(file_path), is_property=True)
290
+ file_arg = f"file={file_val},line=1,"
291
+
292
+ if len(file_issues) == 1:
293
+ issue = file_issues[0]
294
+ clean_msg = self._strip_status_prefix(issue["message"])
295
+ escaped_msg = self._escape(clean_msg)
296
+ else:
297
+ # Combine into a single bulleted annotation
298
+ combined_msg = f"Multiple ruff-sync {level}s in {file_path}:"
299
+ for issue in file_issues:
300
+ clean_line = self._strip_status_prefix(issue["message"])
301
+ combined_msg += f"\n- {clean_line}"
302
+ escaped_msg = self._escape(combined_msg)
303
+
304
+ print(f"::{level} {file_arg}title={title_val}::{escaped_msg}")
305
+
306
+ def _write_summary(self, all_issues: list[GithubIssue]) -> None:
307
+ """Write a Markdown report to the GitHub Step Summary file."""
308
+ summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
309
+ if not summary_file or not all_issues:
310
+ return
311
+
312
+ summary_path = pathlib.Path(summary_file)
313
+ try:
314
+ with summary_path.open("a", encoding="utf-8") as f:
315
+ f.write("### 🔄 Ruff-Sync Drift Report\n\n")
316
+ f.write("| Severity | Key | Message | File |\n")
317
+ f.write("| :--- | :--- | :--- | :--- |\n")
318
+ for issue in all_issues:
319
+ severity = "❌ Error" if issue["level"] == "error" else "⚠️ Warning"
320
+ key = issue["drift_key"] or "-"
321
+ clean_msg = self._strip_status_prefix(issue["message"])
322
+ file_path = issue["file_path"] or self._DEFAULT_PATH
323
+ f.write(f"| {severity} | `{key}` | {clean_msg} | `{file_path.name}` |\n")
324
+ f.write("\n")
325
+ except OSError:
326
+ LOGGER.exception(f"Failed to write to GITHUB_STEP_SUMMARY: {summary_file}")
236
327
 
237
328
 
238
329
  class JsonFormatter:
@@ -26,9 +26,11 @@ if TYPE_CHECKING:
26
26
  # ---------------------------------------------------------------------------
27
27
 
28
28
 
29
- @pytest.fixture(params=[TextFormatter, GithubFormatter, JsonFormatter])
29
+ @pytest.fixture(
30
+ params=[TextFormatter, GithubFormatter, JsonFormatter, GitlabFormatter, SarifFormatter]
31
+ )
30
32
  def formatter(request: pytest.FixtureRequest) -> ResultFormatter:
31
- """Fixture providing instances of all streaming formatters."""
33
+ """Fixture providing instances of ALL available formatters."""
32
34
  formatter_cls: type[ResultFormatter] = request.param
33
35
  return formatter_cls()
34
36
 
@@ -53,26 +55,39 @@ class TestFormatterBasics:
53
55
  ) -> None:
54
56
  """Verify methods that primarily output to stdout."""
55
57
  getattr(formatter, method)(message)
58
+ formatter.finalize()
56
59
  captured = capsys.readouterr().out
57
60
 
58
- if isinstance(formatter, JsonFormatter):
59
- # diff strip()s input in implementation
60
- expected_msg = message.strip() if method == "diff" else message
61
- assert json.loads(captured) == {"level": method, "message": expected_msg}
62
- else:
63
- # Text and Github both print raw for these
64
- assert message in captured
65
-
66
- def test_finalize_is_noop_for_streaming_formatters(
61
+ match formatter:
62
+ case JsonFormatter():
63
+ # diff strip()s input in implementation
64
+ expected_msg = message.strip() if method == "diff" else message
65
+ assert json.loads(captured) == {"level": method, "message": expected_msg}
66
+ case GitlabFormatter() | SarifFormatter():
67
+ # These only output on finalize, and don't output notes/success/diffs
68
+ # (except Gitlab potentially logging them via logger)
69
+ pass
70
+ case _:
71
+ # Text and Github print raw for these (Github prints notes/success immediately)
72
+ assert message in captured
73
+
74
+ def test_finalize_is_safe_and_reproducible(
67
75
  self,
68
76
  formatter: ResultFormatter,
69
77
  capsys: pytest.CaptureFixture[str],
70
78
  ) -> None:
71
- """finalize() must not emit any output for streaming formatters."""
79
+ """finalize() must be safe to call and only produce output for accumulating types."""
72
80
  formatter.finalize()
73
81
  captured = capsys.readouterr()
74
- assert captured.out == ""
75
- assert captured.err == ""
82
+
83
+ match formatter:
84
+ case GithubFormatter() | GitlabFormatter() | SarifFormatter():
85
+ # Accumulating formatters might output empty lists or valid JSON/Markdown
86
+ pass
87
+ case _:
88
+ # Strictly streaming formatters should have no output on empty finalize()
89
+ assert captured.out == ""
90
+ assert captured.err == ""
76
91
 
77
92
  @pytest.mark.parametrize(
78
93
  "method, level",
@@ -97,45 +112,139 @@ class TestFormatterBasics:
97
112
  with caplog.at_level(level, logger="ruff_sync.formatters"):
98
113
  getattr(formatter, method)(message)
99
114
 
115
+ formatter.finalize()
100
116
  captured = capsys.readouterr().out
101
117
 
102
- if isinstance(formatter, TextFormatter):
103
- # TextFormatter only logs, never prints diagnostics
104
- assert captured == ""
105
- assert message in caplog.text
106
- elif isinstance(formatter, GithubFormatter):
107
- # GithubFormatter logs AND prints workflow commands
108
- assert message in caplog.text
109
- if method != "info": # info only logs in GithubFormatter presently
110
- assert f"::{method}" in captured
111
- elif isinstance(formatter, JsonFormatter):
112
- # JsonFormatter only prints JSON
113
- data = json.loads(captured)
114
- assert data["level"] == method
115
- assert data["message"] == message
118
+ match formatter:
119
+ case TextFormatter():
120
+ # TextFormatter only logs, never prints diagnostics
121
+ assert captured == ""
122
+ assert message in caplog.text
123
+ case GithubFormatter():
124
+ # GithubFormatter logs AND prints workflow commands
125
+ assert message in caplog.text
126
+ if method != "info":
127
+ assert f"::{method}" in captured
128
+ case JsonFormatter():
129
+ # JsonFormatter only prints JSON object per diagnostic
130
+ assert message in captured
131
+ case GitlabFormatter():
132
+ # Gitlab only accumulates error/warning
133
+ if method in ("error", "warning"):
134
+ data = json.loads(captured)
135
+ assert any(issue["description"] == message for issue in data)
136
+ case SarifFormatter():
137
+ # Sarif only accumulates error/warning
138
+ if method in ("error", "warning"):
139
+ data = json.loads(captured)
140
+ assert any(r["message"]["text"] == message for r in data["runs"][0]["results"])
116
141
 
117
142
 
118
143
  class TestGithubFormatterSpecifics:
144
+ """Tests for GithubFormatter (GitHub Actions workflow commands)."""
145
+
119
146
  def test_escape(self) -> None:
120
147
  """Verify GitHub-specific character escaping."""
121
148
  assert GithubFormatter._escape("a%b\rc\nd") == "a%25b%0Dc%0Ad"
122
149
  assert GithubFormatter._escape("a:b,c", is_property=True) == "a%3Ab%2Cc"
123
150
 
151
+ def test_note_and_success_are_streaming(self, capsys: pytest.CaptureFixture[str]) -> None:
152
+ """note() and success() should emit output immediately."""
153
+ fmt = GithubFormatter()
154
+ fmt.note("note msg")
155
+ fmt.success("success msg")
156
+ captured = capsys.readouterr().out
157
+ assert "note msg" in captured
158
+ assert "success msg" in captured
159
+
160
+ def test_debug_is_streaming(self, capsys: pytest.CaptureFixture[str]) -> None:
161
+ """debug() should emit ::debug:: immediately."""
162
+ fmt = GithubFormatter()
163
+ fmt.debug("debug msg")
164
+ captured = capsys.readouterr().out
165
+ assert "::debug::debug msg" in captured
166
+
167
+ def test_error_and_warning_are_accumulated(self, capsys: pytest.CaptureFixture[str]) -> None:
168
+ """error() and warning() must not emit output until finalize() is called."""
169
+ fmt = GithubFormatter()
170
+ fmt.error("error msg")
171
+ fmt.warning("warning msg")
172
+
173
+ captured = capsys.readouterr().out
174
+ assert "::error" not in captured
175
+ assert "::warning" not in captured
176
+
177
+ fmt.finalize()
178
+ captured = capsys.readouterr().out
179
+ assert "::error" in captured
180
+ assert "::warning" in captured
181
+
124
182
  def test_emoji_stripping(self, capsys: pytest.CaptureFixture[str]) -> None:
125
183
  """Ensure leading status icons are stripped from GitHub-specific output."""
126
184
  fmt = GithubFormatter()
127
185
  fmt.error("❌ error msg")
128
186
  fmt.warning("⚠️ warning msg")
187
+ fmt.finalize()
129
188
 
130
189
  captured = capsys.readouterr().out
131
190
  assert "title=Ruff Sync Error::error msg" in captured
132
191
  assert "title=Ruff Sync Warning::warning msg" in captured
133
192
 
134
- def test_file_path_metadata(self, capsys: pytest.CaptureFixture[str]) -> None:
135
- """Ensure file path is propagated in GitHub annotations."""
193
+ def test_single_issue_has_precision(self, capsys: pytest.CaptureFixture[str]) -> None:
194
+ """A single issue should still include line=1 and the raw message."""
136
195
  fmt = GithubFormatter()
137
196
  fmt.error("msg", file_path=pathlib.Path("src/foo.py"))
138
- assert "file=src/foo.py" in capsys.readouterr().out
197
+ fmt.finalize()
198
+ captured = capsys.readouterr().out
199
+ assert "file=src/foo.py,line=1," in captured
200
+ assert "::error" in captured
201
+ assert "::msg" in captured
202
+
203
+ def test_multiple_issues_are_combined(self, capsys: pytest.CaptureFixture[str]) -> None:
204
+ """Multiple issues in the same file should be combined into one annotation."""
205
+ fmt = GithubFormatter()
206
+ path = pathlib.Path("pyproject.toml")
207
+ fmt.error("msg 1", file_path=path)
208
+ fmt.error("msg 2", file_path=path)
209
+ fmt.finalize()
210
+
211
+ captured = capsys.readouterr().out
212
+ # Should only have ONE ::error line
213
+ assert captured.count("::error") == 1
214
+ assert "Multiple ruff-sync errors in pyproject.toml" in captured
215
+ assert "msg 1" in captured
216
+ assert "msg 2" in captured
217
+
218
+ def test_step_summary_logic(
219
+ self,
220
+ tmp_path: pathlib.Path,
221
+ capsys: pytest.CaptureFixture[str],
222
+ monkeypatch: pytest.MonkeyPatch,
223
+ ) -> None:
224
+ """Verify the Markdown summary is written to GITHUB_STEP_SUMMARY."""
225
+ summary_file = tmp_path / "summary.md"
226
+ monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary_file))
227
+
228
+ fmt = GithubFormatter()
229
+ fmt.error("error msg", drift_key="lint.select")
230
+ fmt.warning("warn msg", drift_key="target-version")
231
+ fmt.finalize()
232
+
233
+ content = summary_file.read_text()
234
+ assert "### 🔄 Ruff-Sync Drift Report" in content
235
+ assert "❌ Error" in content
236
+ assert "⚠️ Warning" in content
237
+ assert "| `lint.select` | error msg |" in content
238
+ assert "| `target-version` | warn msg |" in content
239
+
240
+ def test_step_summary_no_env(
241
+ self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str]
242
+ ) -> None:
243
+ """Finalize should work fine and output nothing to summary if env var is missing."""
244
+ fmt = GithubFormatter()
245
+ fmt.error("error")
246
+ fmt.finalize()
247
+ # No crash occurs
139
248
 
140
249
 
141
250
  class TestJsonFormatterSpecifics:
@@ -1594,7 +1594,7 @@ wheels = [
1594
1594
 
1595
1595
  [[package]]
1596
1596
  name = "ruff-sync"
1597
- version = "0.1.5.dev4"
1597
+ version = "0.1.5.dev5"
1598
1598
  source = { editable = "." }
1599
1599
  dependencies = [
1600
1600
  { name = "httpx" },