github2gerrit 1.0.9__tar.gz → 1.1.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.1.0}/PKG-INFO +1 -1
  2. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/action.yaml +14 -9
  3. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/cli.py +102 -27
  4. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/core.py +65 -11
  5. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_cli.py +9 -0
  6. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_cli_outputs_file.py +5 -0
  7. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_cli_url_and_dryrun.py +3 -0
  8. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_config_and_errors.py +1 -1
  9. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_integration_fixture_repo.py +8 -2
  10. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_ssrf_protection.py +32 -12
  11. github2gerrit-1.1.0/tests/test_dns_validation_and_no_gerrit.py +475 -0
  12. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/uv.lock +21 -21
  13. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.editorconfig +0 -0
  14. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.gitignore +0 -0
  15. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.gitlint +0 -0
  16. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.markdownlint.yaml +0 -0
  17. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.pre-commit-config.yaml +0 -0
  18. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.readthedocs.yml +0 -0
  19. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/.yamllint +0 -0
  20. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/LICENSE +0 -0
  21. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/LICENSES/Apache-2.0.txt +0 -0
  22. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/README.md +0 -0
  23. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/REUSE.toml +0 -0
  24. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/docs/COMMIT_RULES.md +0 -0
  25. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  26. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  27. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/docs/RELEASE-v0.2.0.md +0 -0
  28. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
  29. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/pyproject.toml +0 -0
  30. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/sitecustomize.py +0 -0
  31. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/__init__.py +0 -0
  32. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/commit_normalization.py +0 -0
  33. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/commit_rules.py +0 -0
  34. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/config.py +0 -0
  35. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/constants.py +0 -0
  36. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/duplicate_detection.py +0 -0
  37. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/error_codes.py +0 -0
  38. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/external_api.py +0 -0
  39. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  40. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gerrit_query.py +0 -0
  41. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gerrit_rest.py +0 -0
  42. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gerrit_urls.py +0 -0
  43. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/github_api.py +0 -0
  44. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gitreview.py +0 -0
  45. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/gitutils.py +0 -0
  46. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/mapping_comment.py +0 -0
  47. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/models.py +0 -0
  48. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/netrc.py +0 -0
  49. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
  50. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  51. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/pr_commands.py +0 -0
  52. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/pr_content_filter.py +0 -0
  53. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/reconcile_matcher.py +0 -0
  54. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/rich_display.py +0 -0
  55. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/rich_logging.py +0 -0
  56. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/similarity.py +0 -0
  57. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
  58. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/ssh_common.py +0 -0
  59. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/ssh_config_parser.py +0 -0
  60. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/ssh_discovery.py +0 -0
  61. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/trailers.py +0 -0
  62. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/src/github2gerrit/utils.py +0 -0
  63. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/conftest.py +0 -0
  64. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/fixtures/__init__.py +0 -0
  65. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/fixtures/make_repo.py +0 -0
  66. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/fixtures/ssh_config_samples.py +0 -0
  67. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_action_environment_mapping.py +0 -0
  68. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_action_outputs.py +0 -0
  69. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_action_pr_number_handling.py +0 -0
  70. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_action_step_validation.py +0 -0
  71. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_automation_only.py +0 -0
  72. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_change_id_deduplication.py +0 -0
  73. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_cli_helpers.py +0 -0
  74. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_cli_netrc_options.py +0 -0
  75. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_commit_normalization.py +0 -0
  76. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_commit_rules.py +0 -0
  77. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_composite_action_coverage.py +0 -0
  78. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_config_and_reviewers.py +0 -0
  79. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_config_helpers.py +0 -0
  80. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_close_pr_policy.py +0 -0
  81. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_gerrit_backref_comment.py +0 -0
  82. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_gerrit_push_errors.py +0 -0
  83. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_gerrit_rest_results.py +0 -0
  84. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_prepare_commits.py +0 -0
  85. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_shallow_clone.py +0 -0
  86. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_core_ssh_setup.py +0 -0
  87. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_duplicate_detection.py +0 -0
  88. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_email_case_normalization.py +0 -0
  89. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_error_codes.py +0 -0
  90. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_external_api_framework.py +0 -0
  91. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_force_flag_cli.py +0 -0
  92. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_change_id_footer.py +0 -0
  93. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_change_status_checks.py +0 -0
  94. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_pr_closer.py +0 -0
  95. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_rest_client.py +0 -0
  96. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_urls.py +0 -0
  97. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gerrit_urls_more.py +0 -0
  98. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ghe_and_gitreview_args.py +0 -0
  99. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_github_api_error_handling.py +0 -0
  100. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_github_api_helpers.py +0 -0
  101. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_github_api_retry_and_helpers.py +0 -0
  102. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gitreview.py +0 -0
  103. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_gitutils_helpers.py +0 -0
  104. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_issue_157_regressions.py +0 -0
  105. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_mapping_comment_additional.py +0 -0
  106. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  107. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_metadata_and_reconciliation.py +0 -0
  108. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
  109. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_misc_small_coverage.py +0 -0
  110. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_netrc.py +0 -0
  111. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_orphan_rest_side_effects.py +0 -0
  112. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_pr_commands.py +0 -0
  113. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_pr_content_filter.py +0 -0
  114. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_pr_content_filter_integration.py +0 -0
  115. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_pr_update_detection.py +0 -0
  116. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_reconciliation_extracted_module.py +0 -0
  117. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  118. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_reconciliation_scenarios.py +0 -0
  119. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_agent.py +0 -0
  120. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_agent_ownership.py +0 -0
  121. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_artifact_prevention.py +0 -0
  122. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_common.py +0 -0
  123. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_discovery.py +0 -0
  124. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_ssh_discovery_dry_run.py +0 -0
  125. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_trailers_additional.py +0 -0
  126. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_url_parser.py +0 -0
  127. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/test_utils.py +0 -0
  128. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/unit/test_config_integration.py +0 -0
  129. {github2gerrit-1.0.9 → github2gerrit-1.1.0}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.0.9
3
+ Version: 1.1.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
@@ -168,6 +172,14 @@ outputs:
168
172
  runs:
169
173
  using: "composite"
170
174
  steps:
175
+ - name: "Checkout repository"
176
+ # yamllint disable-line rule:line-length
177
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
178
+ with:
179
+ fetch-depth: ${{ inputs.FETCH_DEPTH }}
180
+ # Ensure we are on the PR's head SHA when triggered by PR events
181
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
182
+
171
183
  - name: "Setup Python"
172
184
  # yamllint disable-line rule:line-length
173
185
  uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -176,18 +188,10 @@ runs:
176
188
 
177
189
  - name: "Setup uv"
178
190
  # yamllint disable-line rule:line-length
179
- uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0
191
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
180
192
  with:
181
193
  enable-cache: false
182
194
 
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
195
  - name: "Setup github2gerrit"
192
196
  shell: bash
193
197
  env:
@@ -338,6 +342,7 @@ runs:
338
342
  SYNC_ALL_OPEN_PRS: ${{ env.SYNC_ALL_OPEN_PRS }}
339
343
  PR_NUMBER: ${{ env.PR_NUMBER }}
340
344
  G2G_TEST_MODE: "false"
345
+ G2G_NO_GERRIT: ${{ inputs.G2G_NO_GERRIT }}
341
346
  run: |
342
347
  # Run github2gerrit Python CLI
343
348
  set -euo pipefail
@@ -2126,8 +2126,40 @@ def _process() -> None:
2126
2126
  raise converted_error from exc
2127
2127
 
2128
2128
  gh = _read_github_context()
2129
+
2130
+ # --- G2G_NO_GERRIT: leverage DRY_RUN infrastructure ---
2131
+ # G2G_NO_GERRIT reuses the existing DRY_RUN + G2G_DRYRUN_DISABLE_NETWORK
2132
+ # code paths so that all tool logic runs but Gerrit network operations
2133
+ # are no-ops. Cleanup tasks (abandoned-PR / Gerrit-change sweeps) are
2134
+ # also suppressed because they would hit a non-existent server.
2135
+ no_gerrit = env_bool("G2G_NO_GERRIT", False)
2136
+
2137
+ if no_gerrit:
2138
+ log.info(
2139
+ "🧪 G2G_NO_GERRIT enabled: forcing DRY_RUN=true and "
2140
+ "G2G_DRYRUN_DISABLE_NETWORK=true"
2141
+ )
2142
+ os.environ["DRY_RUN"] = "true"
2143
+ os.environ["G2G_DRYRUN_DISABLE_NETWORK"] = "true"
2144
+ # Rebuild inputs so the rest of the pipeline sees dry_run=True
2145
+ data = _load_effective_inputs()
2146
+
2147
+ # Display config AFTER G2G_NO_GERRIT evaluation so the table
2148
+ # reflects the actual runtime values (e.g. DRY_RUN forced true).
2129
2149
  _display_effective_config(data, gh)
2130
2150
 
2151
+ # Log configured Gerrit server if present. DNS validation is
2152
+ # handled by Orchestrator._resolve_gerrit_info() which covers
2153
+ # both explicit GERRIT_SERVER and .gitreview-derived hosts in
2154
+ # a single, consolidated code path.
2155
+ gerrit_host = data.gerrit_server or ""
2156
+ if gerrit_host:
2157
+ log.debug(
2158
+ "Gerrit server '%s' configured; DNS validation "
2159
+ "deferred to Orchestrator",
2160
+ gerrit_host.strip(),
2161
+ )
2162
+
2131
2163
  # Detect PR operation mode for routing
2132
2164
  operation_mode = gh.get_operation_mode()
2133
2165
  if operation_mode != models.PROperationMode.UNKNOWN:
@@ -2145,13 +2177,25 @@ def _process() -> None:
2145
2177
  log.debug("✏️ PR edit event - will sync metadata to Gerrit change")
2146
2178
  elif operation_mode == models.PROperationMode.CLOSE:
2147
2179
  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
- )
2180
+ if no_gerrit:
2181
+ log.debug(
2182
+ "🚪 Pull request #%s closed; Gerrit cleanup skipped "
2183
+ "(G2G_NO_GERRIT)",
2184
+ pr_num,
2185
+ )
2186
+ safe_console_print(
2187
+ f"🚪 Pull request #{pr_num} closed; "
2188
+ "Gerrit cleanup skipped (G2G_NO_GERRIT)"
2189
+ )
2190
+ else:
2191
+ log.debug(
2192
+ "🚪 Pull request #%s closed; performing Gerrit cleanup",
2193
+ pr_num,
2194
+ )
2195
+ safe_console_print(
2196
+ f"🚪 Pull request #{pr_num} closed; "
2197
+ "performing Gerrit cleanup"
2198
+ )
2155
2199
 
2156
2200
  # Debug log prerequisites for abandoning Gerrit change
2157
2201
  log.debug(
@@ -2164,8 +2208,10 @@ def _process() -> None:
2164
2208
  )
2165
2209
 
2166
2210
  # First, abandon the specific Gerrit change for this closed PR
2211
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2167
2212
  if (
2168
- gh.pr_number
2213
+ not no_gerrit
2214
+ and gh.pr_number
2169
2215
  and data.gerrit_server
2170
2216
  and data.gerrit_project
2171
2217
  and gh.repository
@@ -2209,7 +2255,8 @@ def _process() -> None:
2209
2255
  )
2210
2256
 
2211
2257
  # Run abandoned PR cleanup if enabled
2212
- if FORCE_ABANDONED_CLEANUP:
2258
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2259
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2213
2260
  try:
2214
2261
  log.debug("Running abandoned PR cleanup...")
2215
2262
  if gh.repository and "/" in gh.repository:
@@ -2225,7 +2272,8 @@ def _process() -> None:
2225
2272
  log.warning("Abandoned PR cleanup failed: %s", exc)
2226
2273
 
2227
2274
  # Run Gerrit cleanup if enabled
2228
- if FORCE_GERRIT_CLEANUP:
2275
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2276
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2229
2277
  try:
2230
2278
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2231
2279
  if data.gerrit_server and data.gerrit_project:
@@ -2264,10 +2312,14 @@ def _process() -> None:
2264
2312
  log.debug(merge_message)
2265
2313
  safe_console_print(merge_message)
2266
2314
 
2267
- force = env_bool("FORCE", False)
2268
- _process_close_gerrit_change(
2269
- data, gh, gerrit_event_change_url, force=force
2270
- )
2315
+ # Skip in G2G_NO_GERRIT: Gerrit REST calls are not possible
2316
+ if no_gerrit:
2317
+ log.info("G2G_NO_GERRIT: skipping Gerrit change processing")
2318
+ else:
2319
+ force = env_bool("FORCE", False)
2320
+ _process_close_gerrit_change(
2321
+ data, gh, gerrit_event_change_url, force=force
2322
+ )
2271
2323
 
2272
2324
  # Continue with cleanup tasks even if no PR was found/closed
2273
2325
  # (Gerrit change might not have originated from GitHub)
@@ -2276,16 +2328,23 @@ def _process() -> None:
2276
2328
  gerrit_change_url = os.getenv("G2G_GERRIT_CHANGE_URL") or ""
2277
2329
  if gerrit_change_url and not gerrit_event_change_url:
2278
2330
  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)
2331
+ # Skip in G2G_NO_GERRIT: Gerrit REST calls are not possible
2332
+ if no_gerrit:
2333
+ log.info("G2G_NO_GERRIT: skipping Gerrit change processing")
2334
+ else:
2335
+ log.info("Finding and closing source GitHub pull request")
2336
+ force = env_bool("FORCE", False)
2337
+ _process_close_gerrit_change(
2338
+ data, gh, gerrit_change_url, force=force
2339
+ )
2282
2340
 
2283
2341
  # Continue with cleanup tasks
2284
2342
 
2285
2343
  # Run cleanup tasks for Gerrit events and legacy G2G_GERRIT_CHANGE_URL
2286
2344
  if gerrit_event_change_url or gerrit_change_url:
2287
2345
  # Run abandoned PR cleanup if enabled
2288
- if FORCE_ABANDONED_CLEANUP:
2346
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2347
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2289
2348
  try:
2290
2349
  log.debug("Running abandoned PR cleanup...")
2291
2350
  if gh.repository and "/" in gh.repository:
@@ -2301,7 +2360,8 @@ def _process() -> None:
2301
2360
  log.warning("Abandoned PR cleanup failed: %s", exc)
2302
2361
 
2303
2362
  # Run Gerrit cleanup if enabled
2304
- if FORCE_GERRIT_CLEANUP:
2363
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2364
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2305
2365
  try:
2306
2366
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2307
2367
  if data.gerrit_server and data.gerrit_project:
@@ -2322,7 +2382,8 @@ def _process() -> None:
2322
2382
  _process_close_merged_prs(data, gh)
2323
2383
 
2324
2384
  # Run abandoned PR cleanup if enabled
2325
- if FORCE_ABANDONED_CLEANUP:
2385
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2386
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2326
2387
  try:
2327
2388
  log.debug("Running abandoned PR cleanup...")
2328
2389
  if gh.repository and "/" in gh.repository:
@@ -2338,7 +2399,8 @@ def _process() -> None:
2338
2399
  log.warning("Abandoned PR cleanup failed: %s", exc)
2339
2400
 
2340
2401
  # Run Gerrit cleanup if enabled
2341
- if FORCE_GERRIT_CLEANUP:
2402
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2403
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2342
2404
  try:
2343
2405
  log.info("Running Gerrit cleanup for closed GitHub PRs...")
2344
2406
  if data.gerrit_server and data.gerrit_project:
@@ -2383,7 +2445,8 @@ def _process() -> None:
2383
2445
  log.debug("Processing completed ✅")
2384
2446
 
2385
2447
  # Run abandoned PR cleanup if enabled
2386
- if FORCE_ABANDONED_CLEANUP:
2448
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2449
+ if FORCE_ABANDONED_CLEANUP and not no_gerrit:
2387
2450
  try:
2388
2451
  log.debug("Running abandoned PR cleanup...")
2389
2452
  if gh.repository and "/" in gh.repository:
@@ -2399,7 +2462,8 @@ def _process() -> None:
2399
2462
  log.warning("Abandoned PR cleanup failed: %s", exc)
2400
2463
 
2401
2464
  # Run Gerrit cleanup if enabled
2402
- if FORCE_GERRIT_CLEANUP:
2465
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2466
+ if FORCE_GERRIT_CLEANUP and not no_gerrit:
2403
2467
  try:
2404
2468
  log.info("Running Gerrit cleanup for closed GitHub PRs...")
2405
2469
  if data.gerrit_server and data.gerrit_project:
@@ -2567,7 +2631,8 @@ def _process() -> None:
2567
2631
  pipeline_success, result = _process_single(data, gh, progress_tracker)
2568
2632
 
2569
2633
  # Run abandoned PR cleanup if enabled and pipeline was successful
2570
- if pipeline_success and FORCE_ABANDONED_CLEANUP:
2634
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2635
+ if pipeline_success and FORCE_ABANDONED_CLEANUP and not no_gerrit:
2571
2636
  try:
2572
2637
  log.debug("Running abandoned PR cleanup...")
2573
2638
  # Extract owner and repo from gh.repository (format: "owner/repo")
@@ -2585,7 +2650,8 @@ def _process() -> None:
2585
2650
  log.warning("Abandoned PR cleanup failed: %s", exc)
2586
2651
 
2587
2652
  # Run Gerrit cleanup if enabled and pipeline was successful
2588
- if pipeline_success and FORCE_GERRIT_CLEANUP:
2653
+ # Skip in G2G_NO_GERRIT: no Gerrit server to query
2654
+ if pipeline_success and FORCE_GERRIT_CLEANUP and not no_gerrit:
2589
2655
  try:
2590
2656
  log.debug("Running Gerrit cleanup for closed GitHub PRs...")
2591
2657
  if data.gerrit_server and data.gerrit_project:
@@ -2612,10 +2678,11 @@ def _process() -> None:
2612
2678
  # Show summary after progress tracker is stopped
2613
2679
  if show_progress and RICH_AVAILABLE:
2614
2680
  summary = progress_tracker.get_summary() if progress_tracker else {}
2681
+ safe_console_print("")
2615
2682
  safe_console_print(
2616
- "\n✅ Operation completed!"
2683
+ "✅ Operation completed!"
2617
2684
  if pipeline_success
2618
- else "\n❌ Operation failed!",
2685
+ else "❌ Operation failed!",
2619
2686
  style="green" if pipeline_success else "red",
2620
2687
  )
2621
2688
  safe_console_print(
@@ -2864,6 +2931,9 @@ def _get_ssh_agent_status() -> str:
2864
2931
 
2865
2932
  def _display_effective_config(data: Inputs, gh: GitHubContext) -> None:
2866
2933
  """Display effective configuration in a formatted table."""
2934
+ # Use env_bool for consistent boolean parsing across the codebase
2935
+ no_gerrit_enabled = env_bool("G2G_NO_GERRIT", False)
2936
+
2867
2937
  # Detect mode and display prominently
2868
2938
  github_mode = _is_github_mode()
2869
2939
  mode_label = "GITHUB_MODE" if github_mode else "CLI_MODE"
@@ -2924,6 +2994,11 @@ def _display_effective_config(data: Inputs, gh: GitHubContext) -> None:
2924
2994
  # Mode first - always show
2925
2995
  config_info[mode_label] = mode_description
2926
2996
 
2997
+ # Show G2G_NO_GERRIT regardless of operation mode so logs always
2998
+ # indicate when the run is using test infrastructure.
2999
+ if no_gerrit_enabled:
3000
+ config_info["G2G_NO_GERRIT"] = "🧪"
3001
+
2927
3002
  if is_closing_pr_mode:
2928
3003
  # In PR closing mode, only show minimal relevant config
2929
3004
  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:
@@ -180,6 +180,9 @@ def test_no_pr_context_exits_2(tmp_path: Path) -> None:
180
180
  env["GITHUB_EVENT_NAME"] = "workflow_dispatch"
181
181
  # Disable test mode to ensure non-zero exit on missing PR context
182
182
  env.pop("G2G_TEST_MODE", None)
183
+ # Skip early DNS validation — this test targets PR context validation,
184
+ # not network reachability of the derived placeholder hostname
185
+ env["G2G_DRYRUN_DISABLE_NETWORK"] = "true"
183
186
  # Force non-bulk path to avoid GitHub API token requirement
184
187
  env["SYNC_ALL_OPEN_PRS"] = "false"
185
188
  # Set PR_NUMBER to empty to trigger validation error
@@ -238,6 +241,9 @@ def test_validation_fails_when_no_organization_and_missing_gerrit_params(
238
241
  env.pop("GITHUB_EVENT_NAME", None)
239
242
  # Disable test mode to ensure validation errors are properly caught
240
243
  env.pop("G2G_TEST_MODE", None)
244
+ # Skip early DNS validation — this test targets input validation,
245
+ # not network reachability of placeholder hostnames
246
+ env["G2G_DRYRUN_DISABLE_NETWORK"] = "true"
241
247
  # Use empty config file to avoid interference from real config
242
248
  empty_config = tmp_path / "config.txt"
243
249
  empty_config.write_text("", encoding="utf-8")
@@ -294,6 +300,9 @@ def test_validation_local_cli_requires_derivation_disabled(
294
300
  env["G2G_ENABLE_DERIVATION"] = "false" # Explicitly disable derivation
295
301
  # Disable test mode to see validation behavior
296
302
  env.pop("G2G_TEST_MODE", None)
303
+ # Skip early DNS validation — this test targets derivation validation,
304
+ # not network reachability of placeholder hostnames
305
+ env["G2G_DRYRUN_DISABLE_NETWORK"] = "true"
297
306
  # Use empty config file to avoid interference from real config
298
307
  empty_config = tmp_path / "config.txt"
299
308
  empty_config.write_text("", encoding="utf-8")
@@ -111,6 +111,9 @@ def _base_env_with_event(tmp_path: Path) -> dict[str, str]:
111
111
  "GITHUB_HEAD_REF": "feature",
112
112
  # Ensure real execution path (not short-circuited)
113
113
  "G2G_TEST_MODE": "false",
114
+ # Skip early DNS validation — tests mock the Gerrit layer,
115
+ # the derived placeholder hostname is not resolvable
116
+ "G2G_DRYRUN_DISABLE_NETWORK": "true",
114
117
  # Disable automation-only mode for tests
115
118
  "AUTOMATION_ONLY": "false",
116
119
  }
@@ -216,6 +219,8 @@ def test_multi_pr_url_mode_writes_aggregated_outputs(
216
219
  "GITHUB_TOKEN": "dummy",
217
220
  "DRY_RUN": "true",
218
221
  "G2G_TEST_MODE": "false",
222
+ # Skip early DNS validation — tests mock the Gerrit layer
223
+ "G2G_DRYRUN_DISABLE_NETWORK": "true",
219
224
  "GITHUB_OUTPUT": str(outputs_file),
220
225
  }
221
226
 
@@ -31,6 +31,9 @@ def _base_env() -> dict[str, str]:
31
31
  # Token not needed since we mock build_client for bulk mode
32
32
  "GITHUB_TOKEN": "dummy",
33
33
  "DRY_RUN": "true",
34
+ # Skip early DNS validation — tests mock the Gerrit layer,
35
+ # the derived placeholder hostname is not resolvable
36
+ "G2G_DRYRUN_DISABLE_NETWORK": "true",
34
37
  # Disable automation-only mode for tests
35
38
  "AUTOMATION_ONLY": "false",
36
39
  }
@@ -43,7 +43,7 @@ def _minimal_inputs() -> Inputs:
43
43
  dry_run=False,
44
44
  normalise_commit=True,
45
45
  gerrit_server="gerrit.example.org",
46
- gerrit_server_port="29418",
46
+ gerrit_server_port=29418,
47
47
  gerrit_project="example/project",
48
48
  issue_id="",
49
49
  issue_id_lookup_json="",
@@ -114,7 +114,9 @@ def test_derive_repo_names_from_context_fallback(tmp_path: Path) -> None:
114
114
  assert names.project_github == "my-repo-name"
115
115
 
116
116
 
117
- def test_resolve_gerrit_info_prefers_gitreview(tmp_path: Path) -> None:
117
+ def test_resolve_gerrit_info_prefers_gitreview(
118
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
119
+ ) -> None:
118
120
  repo = init_repo(tmp_path / "repo4", default_branch="main")
119
121
  write_gitreview(
120
122
  repo,
@@ -127,6 +129,8 @@ def test_resolve_gerrit_info_prefers_gitreview(tmp_path: Path) -> None:
127
129
  assert gitreview is not None
128
130
  gh = _gh_ctx(repository="org/service-repo", owner="org")
129
131
  names = orch._derive_repo_names(gitreview, gh)
132
+ # Skip DNS validation — fake hostname is not resolvable
133
+ monkeypatch.setenv("G2G_DRYRUN_DISABLE_NETWORK", "true")
130
134
  info = orch._resolve_gerrit_info(gitreview, _minimal_inputs(), names)
131
135
  # Should return the gitreview values directly
132
136
  assert info.host == "gerrit.example.net"
@@ -135,7 +139,7 @@ def test_resolve_gerrit_info_prefers_gitreview(tmp_path: Path) -> None:
135
139
 
136
140
 
137
141
  def test_resolve_gerrit_info_dry_run_uses_derived_project_when_missing(
138
- tmp_path: Path,
142
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
139
143
  ) -> None:
140
144
  repo = init_repo(tmp_path / "repo5", default_branch="main")
141
145
  orch = Orchestrator(workspace=repo.path)
@@ -166,6 +170,8 @@ def test_resolve_gerrit_info_dry_run_uses_derived_project_when_missing(
166
170
  allow_duplicates=inputs.allow_duplicates,
167
171
  ci_testing=inputs.ci_testing,
168
172
  )
173
+ # Skip DNS validation — fake hostname is not resolvable
174
+ monkeypatch.setenv("G2G_DRYRUN_DISABLE_NETWORK", "true")
169
175
  info = orch._resolve_gerrit_info(None, inputs, names)
170
176
  assert info.host == "gerrit.example.org"
171
177
  assert info.port == 29418