github2gerrit 1.2.3__tar.gz → 1.2.4__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 (134) hide show
  1. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.pre-commit-config.yaml +4 -4
  2. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/PKG-INFO +2 -2
  3. github2gerrit-1.2.4/SECURITY.md +77 -0
  4. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/action.yaml +43 -30
  5. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/pyproject.toml +22 -2
  6. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/core.py +6 -2
  7. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/external_api.py +17 -3
  8. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gerrit_pr_closer.py +13 -0
  9. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gerrit_query.py +15 -0
  10. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gerrit_rest.py +98 -4
  11. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/orchestrator/reconciliation.py +26 -2
  12. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/pr_content_filter.py +8 -1
  13. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/utils.py +45 -0
  14. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_action_environment_mapping.py +8 -7
  15. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_action_step_validation.py +8 -3
  16. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_dependency_supersession.py +19 -0
  17. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_external_api_framework.py +47 -0
  18. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_rest_client.py +137 -0
  19. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_urls_more.py +1 -2
  20. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_netrc.py +8 -7
  21. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_orphan_rest_side_effects.py +3 -0
  22. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_reconciliation_plan_and_orphans.py +6 -0
  23. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_discovery.py +9 -3
  24. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_utils.py +81 -0
  25. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/uv.lock +86 -79
  26. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.editorconfig +0 -0
  27. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.gitignore +0 -0
  28. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.gitlint +0 -0
  29. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.markdownlint.yaml +0 -0
  30. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.readthedocs.yml +0 -0
  31. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/.yamllint +0 -0
  32. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/LICENSE +0 -0
  33. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/LICENSES/Apache-2.0.txt +0 -0
  34. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/README.md +0 -0
  35. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/REUSE.toml +0 -0
  36. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/docs/COMMIT_RULES.md +0 -0
  37. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  38. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  39. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/docs/RELEASE-v0.2.0.md +0 -0
  40. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/docs/github2gerrit_token_permissions_classic.png +0 -0
  41. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/sitecustomize.py +0 -0
  42. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/__init__.py +0 -0
  43. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/cli.py +0 -0
  44. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/commit_normalization.py +0 -0
  45. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/commit_rules.py +0 -0
  46. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/config.py +0 -0
  47. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/constants.py +0 -0
  48. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/duplicate_detection.py +0 -0
  49. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/error_codes.py +0 -0
  50. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gerrit_ssh.py +0 -0
  51. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gerrit_urls.py +0 -0
  52. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/github_api.py +0 -0
  53. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gitreview.py +0 -0
  54. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/gitutils.py +0 -0
  55. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/mapping_comment.py +0 -0
  56. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/models.py +0 -0
  57. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/netrc.py +0 -0
  58. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/orchestrator/__init__.py +0 -0
  59. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/pr_commands.py +0 -0
  60. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/reconcile_matcher.py +0 -0
  61. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/rich_display.py +0 -0
  62. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/rich_logging.py +0 -0
  63. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/similarity.py +0 -0
  64. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/ssh_agent_setup.py +0 -0
  65. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/ssh_common.py +0 -0
  66. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/ssh_config_parser.py +0 -0
  67. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/ssh_discovery.py +0 -0
  68. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/src/github2gerrit/trailers.py +0 -0
  69. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/conftest.py +0 -0
  70. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/fixtures/__init__.py +0 -0
  71. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/fixtures/make_repo.py +0 -0
  72. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/fixtures/ssh_config_samples.py +0 -0
  73. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_action_outputs.py +0 -0
  74. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_action_pr_number_handling.py +0 -0
  75. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_automation_only.py +0 -0
  76. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_change_id_deduplication.py +0 -0
  77. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_clean_squash_title.py +0 -0
  78. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_cli.py +0 -0
  79. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_cli_helpers.py +0 -0
  80. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_cli_netrc_options.py +0 -0
  81. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_cli_outputs_file.py +0 -0
  82. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_cli_url_and_dryrun.py +0 -0
  83. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_commit_normalization.py +0 -0
  84. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_commit_rules.py +0 -0
  85. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_composite_action_coverage.py +0 -0
  86. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_config_and_reviewers.py +0 -0
  87. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_config_helpers.py +0 -0
  88. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_close_pr_policy.py +0 -0
  89. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_config_and_errors.py +0 -0
  90. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_gerrit_backref_comment.py +0 -0
  91. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_gerrit_push_errors.py +0 -0
  92. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_gerrit_rest_results.py +0 -0
  93. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_integration_fixture_repo.py +0 -0
  94. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_prepare_commits.py +0 -0
  95. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_shallow_clone.py +0 -0
  96. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_ssh_setup.py +0 -0
  97. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_core_ssrf_protection.py +0 -0
  98. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  99. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_duplicate_detection.py +0 -0
  100. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_email_case_normalization.py +0 -0
  101. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_error_codes.py +0 -0
  102. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_force_flag_cli.py +0 -0
  103. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_change_id_footer.py +0 -0
  104. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_change_status_checks.py +0 -0
  105. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_pr_closer.py +0 -0
  106. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_ssh_abandon.py +0 -0
  107. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gerrit_urls.py +0 -0
  108. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ghe_and_gitreview_args.py +0 -0
  109. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_github_api_error_handling.py +0 -0
  110. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_github_api_helpers.py +0 -0
  111. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_github_api_retry_and_helpers.py +0 -0
  112. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gitreview.py +0 -0
  113. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_gitutils_helpers.py +0 -0
  114. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_issue_157_regressions.py +0 -0
  115. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_mapping_comment_additional.py +0 -0
  116. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  117. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_metadata_and_reconciliation.py +0 -0
  118. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_metadata_trailer_separation_bug.py +0 -0
  119. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_misc_small_coverage.py +0 -0
  120. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_pr_commands.py +0 -0
  121. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_pr_content_filter.py +0 -0
  122. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_pr_content_filter_integration.py +0 -0
  123. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_pr_update_detection.py +0 -0
  124. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_reconciliation_extracted_module.py +0 -0
  125. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_reconciliation_scenarios.py +0 -0
  126. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_agent.py +0 -0
  127. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_agent_ownership.py +0 -0
  128. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_artifact_prevention.py +0 -0
  129. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_common.py +0 -0
  130. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_ssh_discovery_dry_run.py +0 -0
  131. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_trailers_additional.py +0 -0
  132. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/test_url_parser.py +0 -0
  133. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/unit/test_config_integration.py +0 -0
  134. {github2gerrit-1.2.3 → github2gerrit-1.2.4}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -59,7 +59,7 @@ repos:
59
59
  types: [yaml]
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: 0671d8ab202c4ac093b78433ae5baf74f3fc7246 # frozen: v0.15.15
62
+ rev: 3b3f7c3f57fe9925356faf5fe6230835138be230 # frozen: v0.15.17
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -115,18 +115,18 @@ repos:
115
115
 
116
116
  # Requires a mirror, primary repo lacks .pre-commit-hooks.yaml
117
117
  - repo: https://github.com/DetachHead/basedpyright-prek-mirror
118
- rev: 78e6efd50b63647fecb7e65fc7032745d861e2c5 # frozen: 1.39.6
118
+ rev: 5f6f2cb9fa8aec15105f77fd21e8dfe29838b16d # frozen: 1.39.8
119
119
  hooks:
120
120
  - id: basedpyright
121
121
  files: ^src/.+\.py$
122
122
 
123
123
  - repo: https://github.com/lfreleng-actions/gha-workflow-linter
124
- rev: 2c315e461ec3379bf9c1682360fdc1a3899f88c9 # frozen: v1.0.4
124
+ rev: 1a307fd9eed98268b11300ab86b1a107e1d3d1ae # frozen: v1.1.1
125
125
  hooks:
126
126
  - id: gha-workflow-linter
127
127
 
128
128
  - repo: https://github.com/python-jsonschema/check-jsonschema
129
- rev: 943377262562a12b57292fc98fabd7dbf81451fe # frozen: 0.37.2
129
+ rev: 8ef330cbb7204d388aa7a620f9549bcea8009663 # frozen: 0.37.3
130
130
  hooks:
131
131
  - id: check-github-actions
132
132
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.2.3
3
+ Version: 1.2.4
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
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Version Control
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
25
  Requires-Dist: click>=8.1.7
26
- Requires-Dist: cryptography>=46.0.5
26
+ Requires-Dist: cryptography>=48.0.1
27
27
  Requires-Dist: git-review>=2.5.0
28
28
  Requires-Dist: pygerrit2>=2.0.15
29
29
  Requires-Dist: pygithub>=2.8.1
@@ -0,0 +1,77 @@
1
+ <!--
2
+ SPDX-License-Identifier: Apache-2.0
3
+ SPDX-FileCopyrightText: 2026 The Linux Foundation
4
+ -->
5
+
6
+ # Security Policy
7
+
8
+ This document describes the security policy for this repository, including
9
+ which versions receive security updates and how to report vulnerabilities.
10
+
11
+ ## Supported Versions
12
+
13
+ Maintainers develop and merge security fixes on the default branch
14
+ (`main`) and publish those fixes in the latest tagged release. Older
15
+ releases and tags do not receive security updates. Users should track
16
+ the latest tagged release for security patches.
17
+
18
+ | Version | Supported |
19
+ | --------------------- | ------------------ |
20
+ | Latest tagged release | :white_check_mark: |
21
+ | Older releases/tags | :x: |
22
+
23
+ ## Reporting a Vulnerability
24
+
25
+ If you discover a security vulnerability in this project, please report it
26
+ **privately** so that maintainers can investigate and release a fix before
27
+ the issue becomes publicly known.
28
+
29
+ ### Preferred: GitHub Private Vulnerability Reporting
30
+
31
+ Use GitHub's private vulnerability reporting feature:
32
+
33
+ 1. Navigate to the **Security** tab of this repository.
34
+ 2. Click **Report a vulnerability**.
35
+ 3. Provide as much detail as possible (see below).
36
+
37
+ This creates a private advisory visible to maintainers.
38
+
39
+ ### Alternative: Email
40
+
41
+ If you cannot use GitHub's private reporting, send an email to the Linux
42
+ Foundation Release Engineering team at:
43
+
44
+ - **<releng@linuxfoundation.org>**
45
+
46
+ Please do **not** report security vulnerabilities through public GitHub
47
+ issues, discussions, or pull requests.
48
+
49
+ ### What to Include
50
+
51
+ To help maintainers triage and resolve the report, please include:
52
+
53
+ - A clear description of the vulnerability and its potential impact.
54
+ - Steps to reproduce the issue (proof-of-concept code or commands).
55
+ - The affected version(s), commit SHA, or release tag.
56
+ - Any known mitigations or workarounds.
57
+ - Your name and contact details for follow-up (optional).
58
+
59
+ ## Response Process
60
+
61
+ Maintainers will acknowledge receipt of vulnerability reports within
62
+ **5 business days**. We aim to:
63
+
64
+ 1. Confirm the vulnerability and determine its severity.
65
+ 2. Develop and test a fix in a private branch or advisory.
66
+ 3. Coordinate a disclosure timeline with the reporter.
67
+ 4. Release a patched version and publish a security advisory.
68
+
69
+ We follow a responsible disclosure process and credit reporters in the
70
+ published advisory unless they request to remain anonymous.
71
+
72
+ ## Scope
73
+
74
+ This policy covers the source code, configuration, and documentation
75
+ in this repository. Please report vulnerabilities in upstream
76
+ dependencies to their respective maintainers; this project will update
77
+ affected dependencies once fixes become available.
@@ -179,11 +179,13 @@ runs:
179
179
  - name: "Check if GitHub2Gerrit is disabled"
180
180
  id: disabled-check
181
181
  shell: bash
182
+ env:
183
+ INPUT_G2G_DISABLED: ${{ inputs.G2G_DISABLED }}
182
184
  run: |
183
185
  # Check if GitHub2Gerrit is disabled
184
186
  set -euo pipefail
185
187
  DISABLED_ENV="${G2G_DISABLED:-}"
186
- DISABLED_INPUT="${{ inputs.G2G_DISABLED }}"
188
+ DISABLED_INPUT="${INPUT_G2G_DISABLED:-}"
187
189
  # Normalize: accept the same truthy set as env_bool()
188
190
  # (1/true/yes/on, case-insensitive, trimmed)
189
191
  _normalize() {
@@ -205,11 +207,15 @@ runs:
205
207
  - name: "Checkout repository"
206
208
  if: steps.disabled-check.outputs.disabled != 'true'
207
209
  # yamllint disable-line rule:line-length
208
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
210
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
209
211
  with:
210
212
  fetch-depth: ${{ inputs.FETCH_DEPTH }}
211
213
  # Ensure we are on the PR's head SHA when triggered by PR events
212
214
  ref: ${{ github.event.pull_request.head.sha || github.sha }}
215
+ # The action authenticates to Gerrit over SSH and to GitHub via an
216
+ # explicit GITHUB_TOKEN env var, so the checkout token does not need
217
+ # to be persisted into the local git config.
218
+ persist-credentials: false
213
219
 
214
220
  - name: "Setup Python"
215
221
  if: steps.disabled-check.outputs.disabled != 'true'
@@ -221,7 +227,7 @@ runs:
221
227
  - name: "Setup uv"
222
228
  if: steps.disabled-check.outputs.disabled != 'true'
223
229
  # yamllint disable-line rule:line-length
224
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
230
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
225
231
  with:
226
232
  enable-cache: false
227
233
 
@@ -233,15 +239,16 @@ runs:
233
239
  # when git metadata is unavailable
234
240
  # GitHub Actions shallow checkout doesn't include .git history
235
241
  SETUPTOOLS_SCM_PRETEND_VERSION: "0.0.0+dev"
242
+ USE_LOCAL_ACTION: ${{ inputs.USE_LOCAL_ACTION }}
236
243
  run: |
237
244
  # Setup github2gerrit
238
245
  set -euo pipefail
239
246
  uv --version
240
247
  # Install locally for self-testing, use uvx for external repos
241
- if [[ "${{ inputs.USE_LOCAL_ACTION }}" == "true" ]] || \
242
- [[ "${{ github.repository }}" =~ lfreleng-actions/github2gerrit-action ]]; then
243
- echo "Installing with: uv pip install --system ${{ github.action_path }}"
244
- uv pip install --system ${{ github.action_path }}
248
+ if [[ "${USE_LOCAL_ACTION}" == "true" ]] || \
249
+ [[ "${GITHUB_REPOSITORY}" =~ lfreleng-actions/github2gerrit-action ]]; then
250
+ echo "Installing with: uv pip install --system ${GITHUB_ACTION_PATH}"
251
+ uv pip install --system "${GITHUB_ACTION_PATH}"
245
252
  else
246
253
  echo "uvx will install GitHub2Gerrit from PyPI"
247
254
  fi
@@ -258,11 +265,14 @@ runs:
258
265
 
259
266
  - name: "Normalize PR_NUMBER"
260
267
  if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name == 'workflow_dispatch' }}
268
+ id: normalize
261
269
  shell: bash
270
+ env:
271
+ INPUT_PR_NUMBER: ${{ inputs.PR_NUMBER }}
262
272
  run: |
263
273
  # Normalize PR_NUMBER
264
274
  set -euo pipefail
265
- pr_in="${{ inputs.PR_NUMBER }}"
275
+ pr_in="${INPUT_PR_NUMBER:-}"
266
276
  if [[ -z "${pr_in}" || "${pr_in}" == "null" ]]; then
267
277
  pr_in="0"
268
278
  fi
@@ -271,51 +281,53 @@ runs:
271
281
  exit 2
272
282
  fi
273
283
  if [[ "${pr_in}" == "0" ]]; then
274
- echo "SYNC_ALL_OPEN_PRS=true" >> "$GITHUB_ENV"
284
+ echo "sync_all=true" >> "$GITHUB_OUTPUT"
275
285
  else
276
- echo "PR_NUMBER=${pr_in}" >> "$GITHUB_ENV"
286
+ echo "pr_number=${pr_in}" >> "$GITHUB_OUTPUT"
277
287
  fi
278
288
 
279
289
  - name: "Extract PR number, validate context"
280
290
  if: steps.disabled-check.outputs.disabled != 'true'
291
+ id: extract
281
292
  shell: bash
293
+ env:
294
+ EVENT_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || '' }}
295
+ DISPATCH_PR_NUMBER: ${{ steps.normalize.outputs.pr_number }}
296
+ DISPATCH_SYNC_ALL: ${{ steps.normalize.outputs.sync_all }}
282
297
  run: |
283
298
  # Extract PR number, validate context
284
299
  set -euo pipefail
285
300
 
286
301
  # Push events don't need PR_NUMBER (used for closing merged PRs)
287
302
  # The CLI handles push events specially via _process_close_merged_prs()
288
- if [[ "${{ github.event_name }}" == "push" ]]; then
303
+ if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
289
304
  echo "Push event detected - will process merged commits for PR closure"
290
- # Set PR_NUMBER to empty to indicate this is intentional for push events
291
- echo "PR_NUMBER=" >> "$GITHUB_ENV"
305
+ # Emit an empty PR number to signal intentional push handling
306
+ echo "pr_number=" >> "$GITHUB_OUTPUT"
292
307
  exit 0
293
308
  fi
294
309
 
295
- # Honor PR_NUMBER or SYNC_ALL_OPEN_PRS set by workflow_dispatch
296
- if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
297
- if [[ -n "${SYNC_ALL_OPEN_PRS:-}" ]]; then
310
+ # Honor PR_NUMBER or sync-all from workflow_dispatch normalization
311
+ if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
312
+ if [[ -n "${DISPATCH_SYNC_ALL:-}" ]]; then
298
313
  echo "Processing all open pull requests via workflow_dispatch."
314
+ echo "sync_all=true" >> "$GITHUB_OUTPUT"
299
315
  else
300
- if [[ -z "${PR_NUMBER:-}" || "${PR_NUMBER}" == "null" ]]; then
316
+ if [[ -z "${DISPATCH_PR_NUMBER:-}" || "${DISPATCH_PR_NUMBER}" == "null" ]]; then
301
317
  echo "Error: provide PR_NUMBER or set 0 to process all PRs."
302
318
  exit 2
303
319
  fi
304
- echo "PR_NUMBER=${PR_NUMBER}" >> "$GITHUB_ENV"
320
+ echo "pr_number=${DISPATCH_PR_NUMBER}" >> "$GITHUB_OUTPUT"
305
321
  fi
306
322
  else
307
- PR_NUMBER_EVT="${{ github.event.pull_request.number || github.event.issue.number || '' }}"
308
- # Do not override PR_NUMBER if previously set
309
- if [[ -z "${PR_NUMBER:-}" ]]; then
310
- PR_NUMBER="${PR_NUMBER_EVT}"
311
- echo "PR_NUMBER=${PR_NUMBER}" >> "$GITHUB_ENV"
312
- fi
313
- if [[ -z "${PR_NUMBER}" || "${PR_NUMBER}" == "null" ]]; then
323
+ pr_number="${EVENT_PR_NUMBER}"
324
+ if [[ -z "${pr_number}" || "${pr_number}" == "null" ]]; then
314
325
  echo "Error: PR_NUMBER is empty."
315
326
  echo "This action requires a valid pull request context."
316
- echo "Current event: ${{ github.event_name }}"
327
+ echo "Current event: ${GITHUB_EVENT_NAME}"
317
328
  exit 2
318
329
  fi
330
+ echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
319
331
  fi
320
332
 
321
333
  - name: "Run github2gerrit Python CLI"
@@ -375,16 +387,17 @@ runs:
375
387
  GITHUB_BASE_REF: ${{ github.base_ref }}
376
388
  GITHUB_HEAD_REF: ${{ github.head_ref }}
377
389
  GITHUB_ACTOR: ${{ github.actor }}
378
- SYNC_ALL_OPEN_PRS: ${{ env.SYNC_ALL_OPEN_PRS }}
379
- PR_NUMBER: ${{ env.PR_NUMBER }}
390
+ SYNC_ALL_OPEN_PRS: ${{ steps.extract.outputs.sync_all }}
391
+ PR_NUMBER: ${{ steps.extract.outputs.pr_number }}
380
392
  G2G_TEST_MODE: "false"
381
393
  G2G_NO_GERRIT: ${{ inputs.G2G_NO_GERRIT }}
394
+ USE_LOCAL_ACTION: ${{ inputs.USE_LOCAL_ACTION }}
382
395
  run: |
383
396
  # Run github2gerrit Python CLI
384
397
  set -euo pipefail
385
398
  # Use different invocation methods based on repository or USE_LOCAL_ACTION flag
386
- if [[ "${{ inputs.USE_LOCAL_ACTION }}" == "true" ]] || \
387
- [[ "${{ github.repository }}" =~ lfreleng-actions/github2gerrit-action ]]; then
399
+ if [[ "${USE_LOCAL_ACTION}" == "true" ]] || \
400
+ [[ "${GITHUB_REPOSITORY}" =~ lfreleng-actions/github2gerrit-action ]]; then
388
401
  echo "Running:python -m github2gerrit.cli"
389
402
  python -m github2gerrit.cli
390
403
  else
@@ -58,8 +58,13 @@ dependencies = [
58
58
  # Security: Fix CVE-2026-21441 (decompression-bomb vulnerability)
59
59
  "urllib3>=2.6.3",
60
60
 
61
- # Security: Fix CVE-2026-26007 (SECT curve subgroup attack)
62
- "cryptography>=46.0.5",
61
+ # Security: cryptography bundles OpenSSL in its wheels, so OpenSSL
62
+ # security fixes ship as new cryptography releases. Floor at the
63
+ # patched release: fixes CVE-2026-26007 (SECT curve subgroup attack,
64
+ # 46.0.5) and GHSA-537c-gmf6-5ccf (vulnerable OpenSSL in wheels, no
65
+ # CVE assigned, 48.0.1). No upper bound, so future security releases
66
+ # are not held back.
67
+ "cryptography>=48.0.1",
63
68
  ]
64
69
 
65
70
  [project.urls]
@@ -268,6 +273,21 @@ reportUnsupportedDunderAll = "error"
268
273
 
269
274
 
270
275
  [tool.uv]
276
+ # Supply-chain cooldown: do not resolve anything published in the last
277
+ # 7 days (rolling window; uv records it as a relative span in uv.lock).
278
+ # Requires uv >= 0.9.17 for the relative-duration ("7 days") form.
279
+ exclude-newer = "7 days"
280
+ # cryptography bundles OpenSSL directly in its wheels, so OpenSSL security
281
+ # fixes are delivered as new cryptography releases. Exempt it from the
282
+ # supply-chain cooldown above so those patches are not held back by the
283
+ # rolling window.
284
+ #
285
+ # Added 2026-06-15 to unblock GHSA-537c-gmf6-5ccf ("Vulnerable OpenSSL
286
+ # included in cryptography wheels"; no CVE assigned), fixed in 48.0.1,
287
+ # which the 7-day cooldown was excluding. References:
288
+ # https://github.com/advisories/GHSA-537c-gmf6-5ccf
289
+ # https://openssl-library.org/news/secadv/20260609.txt
290
+ exclude-newer-package = { cryptography = "0 days" }
271
291
  # uv will manage installation based on this pyproject and lockfile.
272
292
  # No extra settings are required here; this stanza reserves the
273
293
  # namespace for future use if needed.
@@ -410,9 +410,13 @@ class Orchestrator:
410
410
 
411
411
  # Check if client has authentication
412
412
  if not client.is_authenticated:
413
+ from .gerrit_rest import warn_gerrit_credentials_unavailable
414
+
415
+ warn_gerrit_credentials_unavailable()
413
416
  log.debug(
414
- "Cannot update Gerrit change metadata: "
415
- "No credentials found (check .netrc or environment)"
417
+ "Cannot update Gerrit change metadata for %s: "
418
+ "no Gerrit REST credentials available",
419
+ change_id,
416
420
  )
417
421
  return False
418
422
 
@@ -380,12 +380,26 @@ def external_api_call(
380
380
  reason = (
381
381
  "final attempt" if is_final_attempt else "non-retryable"
382
382
  )
383
- log_exception_conditionally(
384
- log,
383
+ failure_msg = (
385
384
  f"[{api_type.value}] {operation} failed ({reason}) "
386
385
  f"after {attempt} attempt(s) in {duration:.2f}s: "
387
- f"{target}",
386
+ f"{target}"
388
387
  )
388
+ # Authentication/authorization failures (Gerrit REST
389
+ # 401/403) are surfaced once, closer to the request, as a
390
+ # concise warning. Avoid emitting a duplicate error/
391
+ # traceback for them here. Scope this strictly to Gerrit
392
+ # REST: other API types (e.g. GitHub) may also expose a
393
+ # ``.status`` attribute, and their auth/permission
394
+ # failures must retain error-level visibility.
395
+ is_gerrit_auth_failure = (
396
+ api_type == ApiType.GERRIT_REST
397
+ and getattr(exc, "status", None) in (401, 403)
398
+ )
399
+ if is_gerrit_auth_failure:
400
+ log.debug(failure_msg)
401
+ else:
402
+ log_exception_conditionally(log, failure_msg)
389
403
  _update_metrics(api_type, context, success=False, exc=exc)
390
404
  raise
391
405
  else:
@@ -1754,6 +1754,19 @@ def _abandon_gerrit_change(
1754
1754
  abandon_data = {"message": message}
1755
1755
  client.post(abandon_path, data=abandon_data)
1756
1756
  log.debug("Successfully abandoned Gerrit change %s", change_number)
1757
+ except GerritRestError as exc:
1758
+ if exc.is_auth_error:
1759
+ # Expected when no Gerrit REST credentials are available; the
1760
+ # REST layer already surfaced this once. Avoid a duplicate
1761
+ # error-level traceback for an authentication failure.
1762
+ log.debug(
1763
+ "REST abandon for Gerrit change %s failed (HTTP %s)",
1764
+ change_number,
1765
+ exc.status,
1766
+ )
1767
+ else:
1768
+ log.exception("Failed to abandon Gerrit change %s", change_number)
1769
+ raise
1757
1770
  except Exception:
1758
1771
  log.exception("Failed to abandon Gerrit change %s", change_number)
1759
1772
  raise
@@ -13,6 +13,7 @@ from typing import Any
13
13
  from urllib.parse import quote
14
14
 
15
15
  from .gerrit_rest import GerritRestClient
16
+ from .gerrit_rest import warn_gerrit_credentials_unavailable
16
17
 
17
18
 
18
19
  log = logging.getLogger(__name__)
@@ -148,6 +149,20 @@ def query_open_changes_by_project(
148
149
  query = f'project:"{_gerrit_quote(project)}" status:open owner:self'
149
150
  if branch:
150
151
  query += f' branch:"{_gerrit_quote(branch)}"'
152
+
153
+ # The ``owner:self`` predicate requires an authenticated Gerrit
154
+ # session; an anonymous request is rejected with HTTP 403. Skip the
155
+ # query (warning once per run) instead of issuing a request that is
156
+ # guaranteed to fail and would otherwise emit error-level noise.
157
+ if not client.is_authenticated:
158
+ warn_gerrit_credentials_unavailable()
159
+ log.debug(
160
+ "Skipping owner:self query for project '%s': "
161
+ "no Gerrit REST credentials available",
162
+ project,
163
+ )
164
+ return []
165
+
151
166
  log.debug("Querying Gerrit for open changes: %s", query)
152
167
 
153
168
  try:
@@ -46,6 +46,7 @@ from .gerrit_urls import create_gerrit_url_builder
46
46
  from .netrc import GerritCredentials
47
47
  from .netrc import resolve_gerrit_credentials
48
48
  from .utils import log_exception_conditionally
49
+ from .utils import log_warning_once
49
50
 
50
51
 
51
52
  log = logging.getLogger("github2gerrit.gerrit_rest")
@@ -80,12 +81,79 @@ _TRANSIENT_ERR_SUBSTRINGS: Final[tuple[str, ...]] = (
80
81
  "gateway timeout",
81
82
  )
82
83
 
84
+ # HTTP status codes that indicate an authentication/authorization problem
85
+ # rather than a transient fault or a bug. These are expected when no
86
+ # Gerrit REST credentials are available (or they are insufficient for the
87
+ # requested operation), and are surfaced as concise, default-visible
88
+ # warnings rather than error-level tracebacks.
89
+ _AUTH_ERROR_STATUSES: Final[tuple[int, ...]] = (401, 403)
90
+
91
+ # Shared dedup key so the "no Gerrit REST credentials" situation is
92
+ # surfaced at most once per run, regardless of how many auth-gated
93
+ # operations are affected.
94
+ _CREDENTIALS_UNAVAILABLE_KEY: Final[str] = "gerrit_rest_credentials_unavailable"
95
+
96
+
97
+ def _is_auth_status(status: int | None) -> bool:
98
+ """Return True if the HTTP status indicates an auth/authorization error."""
99
+ return status in _AUTH_ERROR_STATUSES
100
+
101
+
102
+ def _extract_http_status(exc: BaseException) -> int | None:
103
+ """Best-effort extraction of an HTTP status code from an exception.
104
+
105
+ Handles both the urllib path (``urllib.error.HTTPError.code``) and the
106
+ pygerrit2/requests path (``HTTPError.response.status_code``).
107
+ """
108
+ code = getattr(exc, "code", None)
109
+ if isinstance(code, int):
110
+ return code
111
+ response = getattr(exc, "response", None)
112
+ status = getattr(response, "status_code", None)
113
+ if isinstance(status, int):
114
+ return status
115
+ return None
116
+
117
+
118
+ def warn_gerrit_credentials_unavailable() -> None:
119
+ """Warn once per run that authenticated Gerrit REST is unavailable.
120
+
121
+ Call this from auth-gated code paths before skipping an operation that
122
+ requires Gerrit REST credentials. The warning is emitted at most once
123
+ per process to avoid log spam while still making the degraded behavior
124
+ visible at the default log level.
125
+ """
126
+ log_warning_once(
127
+ log,
128
+ _CREDENTIALS_UNAVAILABLE_KEY,
129
+ "Gerrit REST credentials are not available; authenticated Gerrit "
130
+ "REST operations are disabled. Fallback behavior may apply and "
131
+ "could degrade performance. Provide GERRIT_HTTP_USER and "
132
+ "GERRIT_HTTP_PASSWORD (or a .netrc entry) to enable authenticated "
133
+ "Gerrit REST access.",
134
+ )
135
+
83
136
 
84
137
  # Removed individual retry logic functions - now using centralized framework
85
138
 
86
139
 
87
140
  class GerritRestError(RuntimeError):
88
- """Raised for non-retryable REST errors or exhausted retries."""
141
+ """Raised for non-retryable REST errors or exhausted retries.
142
+
143
+ Args:
144
+ status: HTTP status code associated with the failure, when known.
145
+ Used by callers to distinguish authentication/authorization
146
+ problems (401/403) from transient or unexpected errors.
147
+ """
148
+
149
+ def __init__(self, *args: object, status: int | None = None) -> None:
150
+ super().__init__(*args)
151
+ self.status = status
152
+
153
+ @property
154
+ def is_auth_error(self) -> bool:
155
+ """Return True if this error stems from an auth failure (401/403)."""
156
+ return _is_auth_status(self.status)
89
157
 
90
158
 
91
159
  @dataclass(frozen=True)
@@ -264,13 +332,38 @@ class GerritRestClient:
264
332
  except urllib.error.HTTPError as http_exc:
265
333
  status = getattr(http_exc, "code", None)
266
334
  msg = f"Gerrit REST {method} {url} failed with HTTP {status}"
267
- log_exception_conditionally(log, msg)
268
- raise GerritRestError(msg) from http_exc
335
+ self._log_request_failure(msg, status)
336
+ raise GerritRestError(msg, status=status) from http_exc
269
337
 
270
338
  except Exception as exc:
339
+ status = _extract_http_status(exc)
271
340
  msg = f"Gerrit REST {method} {url} failed: {exc}"
341
+ self._log_request_failure(msg, status)
342
+ raise GerritRestError(msg, status=status) from exc
343
+
344
+ @staticmethod
345
+ def _log_request_failure(msg: str, status: int | None) -> None:
346
+ """Log a failed REST request at an appropriate level.
347
+
348
+ Authentication/authorization failures (401/403) are expected when
349
+ credentials are missing or insufficient; they are surfaced as a
350
+ single concise warning per run rather than an error-level traceback,
351
+ since they are neither transient faults nor bugs. All other failures
352
+ retain the existing conditional error/traceback behavior.
353
+ """
354
+ if _is_auth_status(status):
355
+ log_warning_once(
356
+ log,
357
+ f"gerrit_rest_auth_{status}",
358
+ "Gerrit REST authentication failed (HTTP %s): the configured "
359
+ "credentials are missing or insufficient for this operation. "
360
+ "Fallback behavior may apply and could degrade performance. "
361
+ "Details: %s",
362
+ status,
363
+ msg,
364
+ )
365
+ else:
272
366
  log_exception_conditionally(log, msg)
273
- raise GerritRestError(msg) from exc
274
367
 
275
368
  def __repr__(self) -> str: # pragma: no cover - convenience
276
369
  masked = ""
@@ -385,4 +478,5 @@ __all__ = [
385
478
  "GerritRestClient",
386
479
  "GerritRestError",
387
480
  "build_client_for_host",
481
+ "warn_gerrit_credentials_unavailable",
388
482
  ]
@@ -511,13 +511,25 @@ def _abandon_orphan_changes(
511
511
 
512
512
  from github2gerrit.gerrit_rest import GerritRestError
513
513
  from github2gerrit.gerrit_rest import build_client_for_host
514
+ from github2gerrit.gerrit_rest import warn_gerrit_credentials_unavailable
514
515
 
515
- abandoned = []
516
+ abandoned: list[str] = []
516
517
  try:
517
518
  client = build_client_for_host(
518
519
  gerrit.host, timeout=10.0, max_attempts=3
519
520
  )
520
521
 
522
+ # Abandoning a change is a mutating REST call that requires
523
+ # authentication; without credentials every attempt would 403.
524
+ # Warn once and skip rather than emitting per-change errors.
525
+ if not client.is_authenticated:
526
+ warn_gerrit_credentials_unavailable()
527
+ log.debug(
528
+ "Skipping orphan-change abandon: "
529
+ "no Gerrit REST credentials available"
530
+ )
531
+ return abandoned
532
+
521
533
  for change_id in orphan_ids:
522
534
  try:
523
535
  abandon_message = (
@@ -559,13 +571,25 @@ def _comment_orphan_changes(
559
571
 
560
572
  from github2gerrit.gerrit_rest import GerritRestError
561
573
  from github2gerrit.gerrit_rest import build_client_for_host
574
+ from github2gerrit.gerrit_rest import warn_gerrit_credentials_unavailable
562
575
 
563
- commented = []
576
+ commented: list[str] = []
564
577
  try:
565
578
  client = build_client_for_host(
566
579
  gerrit.host, timeout=10.0, max_attempts=3
567
580
  )
568
581
 
582
+ # Posting a review comment is a mutating REST call that requires
583
+ # authentication; without credentials every attempt would 403.
584
+ # Warn once and skip rather than emitting per-change errors.
585
+ if not client.is_authenticated:
586
+ warn_gerrit_credentials_unavailable()
587
+ log.debug(
588
+ "Skipping orphan-change comment: "
589
+ "no Gerrit REST credentials available"
590
+ )
591
+ return commented
592
+
569
593
  for change_id in orphan_ids:
570
594
  try:
571
595
  comment_message = (
@@ -42,6 +42,13 @@ _DANGEROUS_HTML_PATTERN = re.compile(
42
42
  )
43
43
  _MULTIPLE_NEWLINES_PATTERN = re.compile(r"\n{3,}")
44
44
  _EMOJI_PATTERN = re.compile(r":[a-z_]+:") # GitHub emoji codes like :sparkles:
45
+ # Dependabot embeds compatibility-score badges proxied through GitHub's
46
+ # camo image host. Detected via a regex rather than a substring membership
47
+ # check (the latter trips CodeQL's incomplete-url-substring-sanitization
48
+ # heuristic and is a weaker way to match a URL).
49
+ _CAMO_IMAGE_URL_PATTERN = re.compile(
50
+ r"https://camo\.githubusercontent\.com/", re.IGNORECASE
51
+ )
45
52
 
46
53
 
47
54
  @dataclass
@@ -112,7 +119,7 @@ class DependabotRule(FilterRule):
112
119
  "Bumps " in title and " from " in title and " to " in title,
113
120
  "Dependabot will resolve any conflicts" in body,
114
121
  "<details>" in body and "<summary>" in body,
115
- "https://camo.githubusercontent.com/" in body,
122
+ bool(_CAMO_IMAGE_URL_PATTERN.search(body)),
116
123
  ]
117
124
 
118
125
  # Require multiple indicators for confidence