github2gerrit 1.0.9__tar.gz → 1.2.0__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 (129) hide show
  1. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.pre-commit-config.yaml +14 -9
  2. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/PKG-INFO +1 -1
  3. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/action.yaml +53 -11
  4. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/pyproject.toml +1 -13
  5. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/cli.py +110 -27
  6. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/core.py +65 -11
  7. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_cli.py +58 -0
  8. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_cli_outputs_file.py +5 -0
  9. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_cli_url_and_dryrun.py +3 -0
  10. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_config_and_errors.py +1 -1
  11. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_integration_fixture_repo.py +8 -2
  12. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_ssrf_protection.py +32 -12
  13. github2gerrit-1.2.0/tests/test_dns_validation_and_no_gerrit.py +475 -0
  14. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/uv.lock +81 -81
  15. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.editorconfig +0 -0
  16. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.gitignore +0 -0
  17. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.gitlint +0 -0
  18. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.markdownlint.yaml +0 -0
  19. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.readthedocs.yml +0 -0
  20. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/.yamllint +0 -0
  21. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/LICENSE +0 -0
  22. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/LICENSES/Apache-2.0.txt +0 -0
  23. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/README.md +0 -0
  24. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/REUSE.toml +0 -0
  25. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/docs/COMMIT_RULES.md +0 -0
  26. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  27. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  28. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/docs/RELEASE-v0.2.0.md +0 -0
  29. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
  30. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/sitecustomize.py +0 -0
  31. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/__init__.py +0 -0
  32. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/commit_normalization.py +0 -0
  33. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/commit_rules.py +0 -0
  34. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/config.py +0 -0
  35. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/constants.py +0 -0
  36. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/duplicate_detection.py +0 -0
  37. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/error_codes.py +0 -0
  38. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/external_api.py +0 -0
  39. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  40. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gerrit_query.py +0 -0
  41. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gerrit_rest.py +0 -0
  42. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gerrit_urls.py +0 -0
  43. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/github_api.py +0 -0
  44. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gitreview.py +0 -0
  45. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/gitutils.py +0 -0
  46. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/mapping_comment.py +0 -0
  47. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/models.py +0 -0
  48. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/netrc.py +0 -0
  49. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
  50. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  51. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/pr_commands.py +0 -0
  52. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/pr_content_filter.py +0 -0
  53. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/reconcile_matcher.py +0 -0
  54. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/rich_display.py +0 -0
  55. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/rich_logging.py +0 -0
  56. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/similarity.py +0 -0
  57. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
  58. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/ssh_common.py +0 -0
  59. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/ssh_config_parser.py +0 -0
  60. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/ssh_discovery.py +0 -0
  61. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/trailers.py +0 -0
  62. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/src/github2gerrit/utils.py +0 -0
  63. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/conftest.py +0 -0
  64. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/fixtures/__init__.py +0 -0
  65. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/fixtures/make_repo.py +0 -0
  66. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/fixtures/ssh_config_samples.py +0 -0
  67. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_action_environment_mapping.py +0 -0
  68. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_action_outputs.py +0 -0
  69. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_action_pr_number_handling.py +0 -0
  70. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_action_step_validation.py +0 -0
  71. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_automation_only.py +0 -0
  72. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_change_id_deduplication.py +0 -0
  73. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_cli_helpers.py +0 -0
  74. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_cli_netrc_options.py +0 -0
  75. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_commit_normalization.py +0 -0
  76. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_commit_rules.py +0 -0
  77. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_composite_action_coverage.py +0 -0
  78. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_config_and_reviewers.py +0 -0
  79. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_config_helpers.py +0 -0
  80. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_close_pr_policy.py +0 -0
  81. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_gerrit_backref_comment.py +0 -0
  82. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_gerrit_push_errors.py +0 -0
  83. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_gerrit_rest_results.py +0 -0
  84. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_prepare_commits.py +0 -0
  85. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_shallow_clone.py +0 -0
  86. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_core_ssh_setup.py +0 -0
  87. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_duplicate_detection.py +0 -0
  88. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_email_case_normalization.py +0 -0
  89. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_error_codes.py +0 -0
  90. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_external_api_framework.py +0 -0
  91. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_force_flag_cli.py +0 -0
  92. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_change_id_footer.py +0 -0
  93. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_change_status_checks.py +0 -0
  94. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_pr_closer.py +0 -0
  95. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_rest_client.py +0 -0
  96. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_urls.py +0 -0
  97. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gerrit_urls_more.py +0 -0
  98. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ghe_and_gitreview_args.py +0 -0
  99. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_github_api_error_handling.py +0 -0
  100. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_github_api_helpers.py +0 -0
  101. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_github_api_retry_and_helpers.py +0 -0
  102. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gitreview.py +0 -0
  103. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_gitutils_helpers.py +0 -0
  104. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_issue_157_regressions.py +0 -0
  105. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_mapping_comment_additional.py +0 -0
  106. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  107. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_metadata_and_reconciliation.py +0 -0
  108. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
  109. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_misc_small_coverage.py +0 -0
  110. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_netrc.py +0 -0
  111. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_orphan_rest_side_effects.py +0 -0
  112. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_pr_commands.py +0 -0
  113. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_pr_content_filter.py +0 -0
  114. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_pr_content_filter_integration.py +0 -0
  115. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_pr_update_detection.py +0 -0
  116. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_reconciliation_extracted_module.py +0 -0
  117. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  118. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_reconciliation_scenarios.py +0 -0
  119. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_agent.py +0 -0
  120. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_agent_ownership.py +0 -0
  121. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_artifact_prevention.py +0 -0
  122. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_common.py +0 -0
  123. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_discovery.py +0 -0
  124. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_ssh_discovery_dry_run.py +0 -0
  125. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_trailers_additional.py +0 -0
  126. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_url_parser.py +0 -0
  127. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/test_utils.py +0 -0
  128. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/unit/test_config_integration.py +0 -0
  129. {github2gerrit-1.0.9 → github2gerrit-1.2.0}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -3,7 +3,7 @@
3
3
  # SPDX-FileCopyrightText: 2025 The Linux Foundation
4
4
 
5
5
  ci:
6
- skip: [pytest]
6
+ skip: [pytest, gha-workflow-linter]
7
7
  autofix_commit_msg: |
8
8
  Chore: pre-commit autofixes
9
9
 
@@ -59,7 +59,7 @@ repos:
59
59
  types: [yaml]
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: b969e2851312ca2b24bbec879ba4954341d1bd12 # frozen: v0.15.5
62
+ rev: e05c5c0818279e5ac248ac9e954431ba58865e61 # frozen: v0.15.7
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -76,13 +76,6 @@ repos:
76
76
  - types-PyYAML
77
77
  - types-requests
78
78
 
79
- # Doesn't seem to run reliably inside pre-commit.ci
80
- # - repo: https://github.com/RobertCraigie/pyright-python
81
- # rev: d393df1703a808473b84bd14a2702f4793014031 # frozen: v1.1.404
82
- # hooks:
83
- # - id: pyright
84
- # files: ^(src|scripts|tests)/.+\.py$
85
-
86
79
  - repo: https://github.com/btford/write-good
87
80
  rev: ab66ce10136dfad5146e69e70f82a3efac8842c1 # frozen: v1.0.8
88
81
  hooks:
@@ -120,6 +113,18 @@ repos:
120
113
  hooks:
121
114
  - id: codespell
122
115
 
116
+ # Requires a mirror, primary repo lacks .pre-commit-hooks.yaml
117
+ - repo: https://github.com/DetachHead/basedpyright-prek-mirror
118
+ rev: d58fe7fc44458fa7e41e2d38c04598a3c87833f2 # frozen: 1.38.3
119
+ hooks:
120
+ - id: basedpyright
121
+ files: ^src/.+\.py$
122
+
123
+ - repo: https://github.com/lfreleng-actions/gha-workflow-linter
124
+ rev: a7caf8f3a1a05688d1cee46615ff94def617e5a3 # frozen: v1.0.2
125
+ hooks:
126
+ - id: gha-workflow-linter
127
+
123
128
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
129
  rev: 8db279a37c552206d2df62269ff6f9d31125815a # frozen: 0.37.0
125
130
  hooks:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.0.9
3
+ Version: 1.2.0
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
@@ -69,6 +69,10 @@ inputs:
69
69
  description: "Enable CI testing mode; overrides .gitreview, creates orphan commits"
70
70
  required: false
71
71
  default: "false"
72
+ G2G_NO_GERRIT:
73
+ description: "Run full pipeline without contacting Gerrit (forces DRY_RUN, suppresses cleanup)"
74
+ required: false
75
+ default: "false"
72
76
  FORCE:
73
77
  description: "Force PR closure regardless of Gerrit change status (abandoned, etc)"
74
78
  required: false
@@ -153,6 +157,10 @@ inputs:
153
157
  description: "Create a Gerrit change when an UPDATE operation cannot find an existing one"
154
158
  required: false
155
159
  default: "false"
160
+ G2G_DISABLED:
161
+ description: "Set to 'true' to disable the action entirely (exits successfully with a message)"
162
+ required: false
163
+ default: ""
156
164
 
157
165
  outputs:
158
166
  gerrit_change_request_url:
@@ -168,27 +176,57 @@ outputs:
168
176
  runs:
169
177
  using: "composite"
170
178
  steps:
179
+ - name: "Check if GitHub2Gerrit is disabled"
180
+ id: disabled-check
181
+ shell: bash
182
+ run: |
183
+ # Check if GitHub2Gerrit is disabled
184
+ set -euo pipefail
185
+ DISABLED_ENV="${G2G_DISABLED:-}"
186
+ DISABLED_INPUT="${{ inputs.G2G_DISABLED }}"
187
+ # Normalize: accept the same truthy set as env_bool()
188
+ # (1/true/yes/on, case-insensitive, trimmed)
189
+ _normalize() {
190
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | xargs
191
+ }
192
+ _is_truthy() {
193
+ case "$(_normalize "$1")" in
194
+ 1|true|yes|on) return 0 ;;
195
+ *) return 1 ;;
196
+ esac
197
+ }
198
+ if _is_truthy "${DISABLED_ENV}" || _is_truthy "${DISABLED_INPUT}"; then
199
+ echo "🛑 GitHub2Gerrit is disabled by check of G2G_DISABLED variable or input"
200
+ echo "disabled=true" >> "$GITHUB_OUTPUT"
201
+ else
202
+ echo "disabled=false" >> "$GITHUB_OUTPUT"
203
+ fi
204
+
205
+ - name: "Checkout repository"
206
+ if: steps.disabled-check.outputs.disabled != 'true'
207
+ # yamllint disable-line rule:line-length
208
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
209
+ with:
210
+ fetch-depth: ${{ inputs.FETCH_DEPTH }}
211
+ # Ensure we are on the PR's head SHA when triggered by PR events
212
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
213
+
171
214
  - name: "Setup Python"
215
+ if: steps.disabled-check.outputs.disabled != 'true'
172
216
  # yamllint disable-line rule:line-length
173
217
  uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
174
218
  with:
175
219
  python-version-file: '${{ github.action_path }}/pyproject.toml'
176
220
 
177
221
  - name: "Setup uv"
222
+ if: steps.disabled-check.outputs.disabled != 'true'
178
223
  # yamllint disable-line rule:line-length
179
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0
224
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
180
225
  with:
181
226
  enable-cache: false
182
227
 
183
- - name: "Checkout repository"
184
- # yamllint disable-line rule:line-length
185
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
186
- with:
187
- fetch-depth: ${{ inputs.FETCH_DEPTH }}
188
- # Ensure we are on the PR's head SHA when triggered by PR events
189
- ref: ${{ github.event.pull_request.head.sha || github.sha }}
190
-
191
228
  - name: "Setup github2gerrit"
229
+ if: steps.disabled-check.outputs.disabled != 'true'
192
230
  shell: bash
193
231
  env:
194
232
  # Provide version for hatch-vcs (via setuptools-scm)
@@ -210,7 +248,7 @@ runs:
210
248
 
211
249
  - name: "Validate PR_NUMBER usage"
212
250
  # yamllint disable-line rule:line-length
213
- if: ${{ github.event_name != 'workflow_dispatch' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
251
+ if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name != 'workflow_dispatch' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
214
252
  shell: bash
215
253
  run: |
216
254
  # Validate PR_NUMBER usage
@@ -219,7 +257,7 @@ runs:
219
257
  exit 2
220
258
 
221
259
  - name: "Normalize PR_NUMBER"
222
- if: ${{ github.event_name == 'workflow_dispatch' }}
260
+ if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name == 'workflow_dispatch' }}
223
261
  shell: bash
224
262
  run: |
225
263
  # Normalize PR_NUMBER
@@ -239,6 +277,7 @@ runs:
239
277
  fi
240
278
 
241
279
  - name: "Extract PR number, validate context"
280
+ if: steps.disabled-check.outputs.disabled != 'true'
242
281
  shell: bash
243
282
  run: |
244
283
  # Extract PR number, validate context
@@ -280,6 +319,7 @@ runs:
280
319
  fi
281
320
 
282
321
  - name: "Run github2gerrit Python CLI"
322
+ if: steps.disabled-check.outputs.disabled != 'true'
283
323
  id: run-cli
284
324
  shell: bash
285
325
  env:
@@ -338,6 +378,7 @@ runs:
338
378
  SYNC_ALL_OPEN_PRS: ${{ env.SYNC_ALL_OPEN_PRS }}
339
379
  PR_NUMBER: ${{ env.PR_NUMBER }}
340
380
  G2G_TEST_MODE: "false"
381
+ G2G_NO_GERRIT: ${{ inputs.G2G_NO_GERRIT }}
341
382
  run: |
342
383
  # Run github2gerrit Python CLI
343
384
  set -euo pipefail
@@ -352,6 +393,7 @@ runs:
352
393
  fi
353
394
 
354
395
  - name: "Capture outputs"
396
+ if: steps.disabled-check.outputs.disabled != 'true'
355
397
  id: capture-outputs
356
398
  shell: bash
357
399
  # yamllint disable rule:line-length
@@ -235,19 +235,7 @@ markers = [
235
235
 
236
236
  [tool.pyright]
237
237
  pythonVersion = "3.11"
238
- include = ["src", "tests"]
239
- exclude = [
240
- "build/",
241
- "dist/",
242
- "docs/",
243
- "scripts/",
244
- "tests/fixtures/",
245
- ".venv/",
246
- ".mypy_cache/",
247
- ".pytest_cache/",
248
- ".ruff_cache/",
249
- "coverage_html_report/",
250
- ]
238
+ include = ["src"]
251
239
  typeCheckingMode = "strict"
252
240
  reportMissingImports = "none"
253
241
  reportMissingTypeStubs = "none"
@@ -845,6 +845,14 @@ def main(
845
845
  typer.echo("Version information not available")
846
846
  sys.exit(int(ExitCode.SUCCESS))
847
847
 
848
+ # Check if GitHub2Gerrit is disabled via environment variable
849
+ if env_bool("G2G_DISABLED"):
850
+ typer.echo(
851
+ "\U0001f6d1 GitHub2Gerrit is disabled by check of "
852
+ "G2G_DISABLED variable"
853
+ )
854
+ sys.exit(int(ExitCode.SUCCESS))
855
+
848
856
  # Override boolean parameters with properly parsed environment variables.
849
857
  # This ensures that string "false" from GitHub Actions is handled
850
858
  # correctly (Typer/Click treats any non-empty string as truthy).
@@ -2126,8 +2134,40 @@ def _process() -> None:
2126
2134
  raise converted_error from exc
2127
2135
 
2128
2136
  gh = _read_github_context()
2137
+
2138
+ # --- G2G_NO_GERRIT: leverage DRY_RUN infrastructure ---
2139
+ # G2G_NO_GERRIT reuses the existing DRY_RUN + G2G_DRYRUN_DISABLE_NETWORK
2140
+ # code paths so that all tool logic runs but Gerrit network operations
2141
+ # are no-ops. Cleanup tasks (abandoned-PR / Gerrit-change sweeps) are
2142
+ # also suppressed because they would hit a non-existent server.
2143
+ no_gerrit = env_bool("G2G_NO_GERRIT", False)
2144
+
2145
+ if no_gerrit:
2146
+ log.info(
2147
+ "🧪 G2G_NO_GERRIT enabled: forcing DRY_RUN=true and "
2148
+ "G2G_DRYRUN_DISABLE_NETWORK=true"
2149
+ )
2150
+ os.environ["DRY_RUN"] = "true"
2151
+ os.environ["G2G_DRYRUN_DISABLE_NETWORK"] = "true"
2152
+ # Rebuild inputs so the rest of the pipeline sees dry_run=True
2153
+ data = _load_effective_inputs()
2154
+
2155
+ # Display config AFTER G2G_NO_GERRIT evaluation so the table
2156
+ # reflects the actual runtime values (e.g. DRY_RUN forced true).
2129
2157
  _display_effective_config(data, gh)
2130
2158
 
2159
+ # Log configured Gerrit server if present. DNS validation is
2160
+ # handled by Orchestrator._resolve_gerrit_info() which covers
2161
+ # both explicit GERRIT_SERVER and .gitreview-derived hosts in
2162
+ # a single, consolidated code path.
2163
+ gerrit_host = data.gerrit_server or ""
2164
+ if gerrit_host:
2165
+ log.debug(
2166
+ "Gerrit server '%s' configured; DNS validation "
2167
+ "deferred to Orchestrator",
2168
+ gerrit_host.strip(),
2169
+ )
2170
+
2131
2171
  # Detect PR operation mode for routing
2132
2172
  operation_mode = gh.get_operation_mode()
2133
2173
  if operation_mode != models.PROperationMode.UNKNOWN:
@@ -2145,13 +2185,25 @@ def _process() -> None:
2145
2185
  log.debug("✏️ PR edit event - will sync metadata to Gerrit change")
2146
2186
  elif operation_mode == models.PROperationMode.CLOSE:
2147
2187
  pr_num = gh.pr_number or "unknown"
2148
- log.debug(
2149
- "🚪 Pull request #%s closed; performing Gerrit cleanup",
2150
- pr_num,
2151
- )
2152
- safe_console_print(
2153
- f"🚪 Pull request #{pr_num} closed; performing Gerrit cleanup"
2154
- )
2188
+ if no_gerrit:
2189
+ log.debug(
2190
+ "🚪 Pull request #%s closed; Gerrit cleanup skipped "
2191
+ "(G2G_NO_GERRIT)",
2192
+ pr_num,
2193
+ )
2194
+ safe_console_print(
2195
+ f"🚪 Pull request #{pr_num} closed; "
2196
+ "Gerrit cleanup skipped (G2G_NO_GERRIT)"
2197
+ )
2198
+ else:
2199
+ log.debug(
2200
+ "🚪 Pull request #%s closed; performing Gerrit cleanup",
2201
+ pr_num,
2202
+ )
2203
+ safe_console_print(
2204
+ f"🚪 Pull request #{pr_num} closed; "
2205
+ "performing Gerrit cleanup"
2206
+ )
2155
2207
 
2156
2208
  # Debug log prerequisites for abandoning Gerrit change
2157
2209
  log.debug(
@@ -2164,8 +2216,10 @@ def _process() -> None:
2164
2216
  )
2165
2217
 
2166
2218
  # First, abandon the specific Gerrit change for this closed PR
2219
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2167
2220
  if (
2168
- gh.pr_number
2221
+ not no_gerrit
2222
+ and gh.pr_number
2169
2223
  and data.gerrit_server
2170
2224
  and data.gerrit_project
2171
2225
  and gh.repository
@@ -2209,7 +2263,8 @@ def _process() -> None:
2209
2263
  )
2210
2264
 
2211
2265
  # Run abandoned PR cleanup if enabled
2212
- if FORCE_ABANDONED_CLEANUP:
2266
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2267
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2213
2268
  try:
2214
2269
  log.debug("Running abandoned PR cleanup...")
2215
2270
  if gh.repository and "/" in gh.repository:
@@ -2225,7 +2280,8 @@ def _process() -> None:
2225
2280
  log.warning("Abandoned PR cleanup failed: %s", exc)
2226
2281
 
2227
2282
  # Run Gerrit cleanup if enabled
2228
- if FORCE_GERRIT_CLEANUP:
2283
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2284
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2229
2285
  try:
2230
2286
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2231
2287
  if data.gerrit_server and data.gerrit_project:
@@ -2264,10 +2320,14 @@ def _process() -> None:
2264
2320
  log.debug(merge_message)
2265
2321
  safe_console_print(merge_message)
2266
2322
 
2267
- force = env_bool("FORCE", False)
2268
- _process_close_gerrit_change(
2269
- data, gh, gerrit_event_change_url, force=force
2270
- )
2323
+ # Skip in G2G_NO_GERRIT: Gerrit REST calls are not possible
2324
+ if no_gerrit:
2325
+ log.info("G2G_NO_GERRIT: skipping Gerrit change processing")
2326
+ else:
2327
+ force = env_bool("FORCE", False)
2328
+ _process_close_gerrit_change(
2329
+ data, gh, gerrit_event_change_url, force=force
2330
+ )
2271
2331
 
2272
2332
  # Continue with cleanup tasks even if no PR was found/closed
2273
2333
  # (Gerrit change might not have originated from GitHub)
@@ -2276,16 +2336,23 @@ def _process() -> None:
2276
2336
  gerrit_change_url = os.getenv("G2G_GERRIT_CHANGE_URL") or ""
2277
2337
  if gerrit_change_url and not gerrit_event_change_url:
2278
2338
  log.info("🔄 Gerrit change URL provided: %s", gerrit_change_url)
2279
- log.info("Finding and closing source GitHub pull request")
2280
- force = env_bool("FORCE", False)
2281
- _process_close_gerrit_change(data, gh, gerrit_change_url, force=force)
2339
+ # Skip in G2G_NO_GERRIT: Gerrit REST calls are not possible
2340
+ if no_gerrit:
2341
+ log.info("G2G_NO_GERRIT: skipping Gerrit change processing")
2342
+ else:
2343
+ log.info("Finding and closing source GitHub pull request")
2344
+ force = env_bool("FORCE", False)
2345
+ _process_close_gerrit_change(
2346
+ data, gh, gerrit_change_url, force=force
2347
+ )
2282
2348
 
2283
2349
  # Continue with cleanup tasks
2284
2350
 
2285
2351
  # Run cleanup tasks for Gerrit events and legacy G2G_GERRIT_CHANGE_URL
2286
2352
  if gerrit_event_change_url or gerrit_change_url:
2287
2353
  # Run abandoned PR cleanup if enabled
2288
- if FORCE_ABANDONED_CLEANUP:
2354
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2355
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2289
2356
  try:
2290
2357
  log.debug("Running abandoned PR cleanup...")
2291
2358
  if gh.repository and "/" in gh.repository:
@@ -2301,7 +2368,8 @@ def _process() -> None:
2301
2368
  log.warning("Abandoned PR cleanup failed: %s", exc)
2302
2369
 
2303
2370
  # Run Gerrit cleanup if enabled
2304
- if FORCE_GERRIT_CLEANUP:
2371
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2372
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2305
2373
  try:
2306
2374
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2307
2375
  if data.gerrit_server and data.gerrit_project:
@@ -2322,7 +2390,8 @@ def _process() -> None:
2322
2390
  _process_close_merged_prs(data, gh)
2323
2391
 
2324
2392
  # Run abandoned PR cleanup if enabled
2325
- if FORCE_ABANDONED_CLEANUP:
2393
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2394
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2326
2395
  try:
2327
2396
  log.debug("Running abandoned PR cleanup...")
2328
2397
  if gh.repository and "/" in gh.repository:
@@ -2338,7 +2407,8 @@ def _process() -> None:
2338
2407
  log.warning("Abandoned PR cleanup failed: %s", exc)
2339
2408
 
2340
2409
  # Run Gerrit cleanup if enabled
2341
- if FORCE_GERRIT_CLEANUP:
2410
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2411
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2342
2412
  try:
2343
2413
  log.info("Running Gerrit cleanup for closed GitHub PRs...")
2344
2414
  if data.gerrit_server and data.gerrit_project:
@@ -2383,7 +2453,8 @@ def _process() -> None:
2383
2453
  log.debug("Processing completed ✅")
2384
2454
 
2385
2455
  # Run abandoned PR cleanup if enabled
2386
- if FORCE_ABANDONED_CLEANUP:
2456
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2457
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2387
2458
  try:
2388
2459
  log.debug("Running abandoned PR cleanup...")
2389
2460
  if gh.repository and "/" in gh.repository:
@@ -2399,7 +2470,8 @@ def _process() -> None:
2399
2470
  log.warning("Abandoned PR cleanup failed: %s", exc)
2400
2471
 
2401
2472
  # Run Gerrit cleanup if enabled
2402
- if FORCE_GERRIT_CLEANUP:
2473
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2474
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2403
2475
  try:
2404
2476
  log.info("Running Gerrit cleanup for closed GitHub PRs...")
2405
2477
  if data.gerrit_server and data.gerrit_project:
@@ -2567,7 +2639,8 @@ def _process() -> None:
2567
2639
  pipeline_success, result = _process_single(data, gh, progress_tracker)
2568
2640
 
2569
2641
  # Run abandoned PR cleanup if enabled and pipeline was successful
2570
- if pipeline_success and FORCE_ABANDONED_CLEANUP:
2642
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2643
+ if pipeline_success and FORCE_ABANDONED_CLEANUP and not no_gerrit:
2571
2644
  try:
2572
2645
  log.debug("Running abandoned PR cleanup...")
2573
2646
  # Extract owner and repo from gh.repository (format: "owner/repo")
@@ -2585,7 +2658,8 @@ def _process() -> None:
2585
2658
  log.warning("Abandoned PR cleanup failed: %s", exc)
2586
2659
 
2587
2660
  # Run Gerrit cleanup if enabled and pipeline was successful
2588
- if pipeline_success and FORCE_GERRIT_CLEANUP:
2661
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2662
+ if pipeline_success and FORCE_GERRIT_CLEANUP and not no_gerrit:
2589
2663
  try:
2590
2664
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2591
2665
  if data.gerrit_server and data.gerrit_project:
@@ -2612,10 +2686,11 @@ def _process() -> None:
2612
2686
  # Show summary after progress tracker is stopped
2613
2687
  if show_progress and RICH_AVAILABLE:
2614
2688
  summary = progress_tracker.get_summary() if progress_tracker else {}
2689
+ safe_console_print("")
2615
2690
  safe_console_print(
2616
- "\n✅ Operation completed!"
2691
+ "✅ Operation completed!"
2617
2692
  if pipeline_success
2618
- else "\n❌ Operation failed!",
2693
+ else "❌ Operation failed!",
2619
2694
  style="green" if pipeline_success else "red",
2620
2695
  )
2621
2696
  safe_console_print(
@@ -2864,6 +2939,9 @@ def _get_ssh_agent_status() -> str:
2864
2939
 
2865
2940
  def _display_effective_config(data: Inputs, gh: GitHubContext) -> None:
2866
2941
  """Display effective configuration in a formatted table."""
2942
+ # Use env_bool for consistent boolean parsing across the codebase
2943
+ no_gerrit_enabled = env_bool("G2G_NO_GERRIT", False)
2944
+
2867
2945
  # Detect mode and display prominently
2868
2946
  github_mode = _is_github_mode()
2869
2947
  mode_label = "GITHUB_MODE" if github_mode else "CLI_MODE"
@@ -2924,6 +3002,11 @@ def _display_effective_config(data: Inputs, gh: GitHubContext) -> None:
2924
3002
  # Mode first - always show
2925
3003
  config_info[mode_label] = mode_description
2926
3004
 
3005
+ # Show G2G_NO_GERRIT regardless of operation mode so logs always
3006
+ # indicate when the run is using test infrastructure.
3007
+ if no_gerrit_enabled:
3008
+ config_info["G2G_NO_GERRIT"] = "🧪"
3009
+
2927
3010
  if is_closing_pr_mode:
2928
3011
  # In PR closing mode, only show minimal relevant config
2929
3012
  if data.dry_run:
@@ -116,12 +116,16 @@ log = logging.getLogger("github2gerrit.core")
116
116
  _MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
117
117
  _MSG_MISSING_PR_CONTEXT = "missing PR context"
118
118
  _MSG_BAD_REPOSITORY_CONTEXT = "bad repository context"
119
- _MSG_MISSING_GERRIT_SERVER = "missing GERRIT_SERVER"
119
+ _MSG_MISSING_GERRIT_SERVER = (
120
+ "Missing Gerrit host. Provide it via the GERRIT_SERVER "
121
+ "input/environment variable, .gitreview file, or action configuration."
122
+ )
120
123
  _MSG_MISSING_GERRIT_PROJECT = "missing GERRIT_PROJECT"
121
124
  _MSG_PYGERRIT2_REQUIRED_REST = "pygerrit2 is required to query Gerrit REST API"
122
125
  _MSG_PYGERRIT2_REQUIRED_AUTH = "pygerrit2 is required for HTTP authentication"
123
126
  _MSG_PYGERRIT2_MISSING = "pygerrit2 missing"
124
127
  _MSG_PYGERRIT2_AUTH_MISSING = "pygerrit2 auth missing"
128
+ _MSG_DNS_RESOLUTION_FAILED = "DNS resolution failed for '%s'"
125
129
 
126
130
 
127
131
  # Removed _insert_issue_id_into_commit_message - dead code
@@ -1658,6 +1662,38 @@ class Orchestrator:
1658
1662
  # Public API
1659
1663
  # ---------------
1660
1664
 
1665
+ def validate_gerrit_server(self, gerrit_host: str | None) -> None:
1666
+ """Validate that the Gerrit server hostname can be resolved via DNS.
1667
+
1668
+ This provides a fast-fail mechanism to catch invalid or
1669
+ unresolvable Gerrit server hostnames early, before any work is done.
1670
+
1671
+ Args:
1672
+ gerrit_host: The Gerrit server hostname to validate.
1673
+ May be ``None`` or empty, in which case an error is raised.
1674
+
1675
+ Raises:
1676
+ OrchestratorError: If the hostname is empty/None or cannot
1677
+ be resolved.
1678
+ """
1679
+ if not gerrit_host or not gerrit_host.strip():
1680
+ raise OrchestratorError(_MSG_MISSING_GERRIT_SERVER)
1681
+
1682
+ host = gerrit_host.strip()
1683
+ try:
1684
+ socket.getaddrinfo(host, None)
1685
+ log.debug("DNS resolution for Gerrit host '%s' succeeded", host)
1686
+ except (OSError, UnicodeError) as exc:
1687
+ log.debug(
1688
+ "Gerrit server '%s' could not be resolved via DNS. "
1689
+ "This typically means either the server hostname is "
1690
+ "incorrect or there is no Gerrit server associated "
1691
+ "with this repository.",
1692
+ host,
1693
+ exc_info=True,
1694
+ )
1695
+ raise OrchestratorError(_MSG_DNS_RESOLUTION_FAILED % host) from exc
1696
+
1661
1697
  def execute(
1662
1698
  self,
1663
1699
  inputs: Inputs,
@@ -2058,7 +2094,15 @@ class Orchestrator:
2058
2094
  inputs: Inputs,
2059
2095
  repo: RepoNames,
2060
2096
  ) -> GerritInfo:
2061
- """Resolve Gerrit connection info from .gitreview or inputs."""
2097
+ """Resolve Gerrit connection info from .gitreview or inputs.
2098
+
2099
+ After resolution, the Gerrit host is validated via DNS to
2100
+ catch bogus hostnames early — regardless of whether the host
2101
+ came from ``.gitreview`` or explicit ``GERRIT_SERVER`` input.
2102
+
2103
+ DNS validation is skipped when ``G2G_DRYRUN_DISABLE_NETWORK``
2104
+ is set, consistent with the preflight and CLI-level guards.
2105
+ """
2062
2106
  log.debug(
2063
2107
  "_resolve_gerrit_info: inputs.ci_testing=%s", inputs.ci_testing
2064
2108
  )
@@ -2071,6 +2115,7 @@ class Orchestrator:
2071
2115
 
2072
2116
  if gitreview:
2073
2117
  log.debug("Using .gitreview settings: %s", gitreview)
2118
+ self._validate_resolved_gerrit_host(gitreview.host)
2074
2119
  return gitreview
2075
2120
 
2076
2121
  host = inputs.gerrit_server.strip()
@@ -2101,8 +2146,24 @@ class Orchestrator:
2101
2146
 
2102
2147
  info = make_gitreview_info(host=host, port=port, project=project)
2103
2148
  log.debug("Resolved Gerrit info: %s", info)
2149
+ self._validate_resolved_gerrit_host(info.host)
2104
2150
  return info
2105
2151
 
2152
+ def _validate_resolved_gerrit_host(self, host: str | None) -> None:
2153
+ """Validate a resolved Gerrit host via DNS unless network is disabled.
2154
+
2155
+ Delegates to :meth:`validate_gerrit_server` for the actual DNS
2156
+ check. Skipped when ``G2G_DRYRUN_DISABLE_NETWORK`` is set so
2157
+ that offline / test scenarios are not penalised.
2158
+ """
2159
+ if env_bool("G2G_DRYRUN_DISABLE_NETWORK", False):
2160
+ log.debug(
2161
+ "Skipping DNS validation for '%s' (G2G_DRYRUN_DISABLE_NETWORK)",
2162
+ host,
2163
+ )
2164
+ return
2165
+ self.validate_gerrit_server(host)
2166
+
2106
2167
  def _setup_ssh(self, inputs: Inputs, gerrit: GerritInfo) -> None:
2107
2168
  """Set up temporary SSH configuration for Gerrit access.
2108
2169
 
@@ -5772,15 +5833,8 @@ class Orchestrator:
5772
5833
  )
5773
5834
  return
5774
5835
 
5775
- # DNS resolution for Gerrit host
5776
- try:
5777
- socket.getaddrinfo(gerrit.host, None)
5778
- log.debug(
5779
- "DNS resolution for Gerrit host '%s' succeeded", gerrit.host
5780
- )
5781
- except Exception as exc:
5782
- msg = "DNS resolution failed"
5783
- raise OrchestratorError(msg) from exc
5836
+ # DNS resolution for Gerrit host (reuses validate_gerrit_server)
5837
+ self.validate_gerrit_server(gerrit.host)
5784
5838
 
5785
5839
  # SSH (TCP) reachability on Gerrit port
5786
5840
  try: