{"id":30735526,"url":"https://github.com/lfreleng-actions/github2gerrit-action","last_synced_at":"2026-06-11T08:31:29.980Z","repository":{"id":311604568,"uuid":"1043645675","full_name":"lfreleng-actions/github2gerrit-action","owner":"lfreleng-actions","description":"Python tool for converting Github pull requests to Gerrit changes, with GitHub action wrapper","archived":false,"fork":false,"pushed_at":"2026-06-03T23:31:50.000Z","size":2600,"stargazers_count":0,"open_issues_count":1,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T02:11:54.958Z","etag":null,"topics":["ci","gerrit","github","python","workflow"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lfreleng-actions.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-24T10:12:10.000Z","updated_at":"2026-06-03T11:19:23.000Z","dependencies_parsed_at":"2026-04-13T10:01:39.740Z","dependency_job_id":null,"html_url":"https://github.com/lfreleng-actions/github2gerrit-action","commit_stats":null,"previous_names":["lfreleng-actions/github2gerrit","lfreleng-actions/github2gerrit-action"],"tags_count":33,"template":false,"template_full_name":"lfreleng-actions/actions-template","purl":"pkg:github/lfreleng-actions/github2gerrit-action","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfreleng-actions%2Fgithub2gerrit-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfreleng-actions%2Fgithub2gerrit-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfreleng-actions%2Fgithub2gerrit-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfreleng-actions%2Fgithub2gerrit-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lfreleng-actions","download_url":"https://codeload.github.com/lfreleng-actions/github2gerrit-action/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfreleng-actions%2Fgithub2gerrit-action/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34190583,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ci","gerrit","github","python","workflow"],"created_at":"2025-09-03T20:03:08.555Z","updated_at":"2026-06-11T08:31:29.972Z","avatar_url":"https://github.com/lfreleng-actions.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!--\nSPDX-License-Identifier: Apache-2.0\nSPDX-FileCopyrightText: 2025 The Linux Foundation\n--\u003e\n\n# github2gerrit\n\nSubmit a GitHub pull request to a Gerrit repository, implemented in Python.\n\nThis action is a drop‑in replacement for the shell‑based\n`lfit/github2gerrit` composite action. It mirrors the same inputs,\noutputs, environment variables, and secrets so you can adopt it without\nchanging existing configuration in your organizations.\n\nThe tool expects a `.gitreview` file in the repository to derive Gerrit\nconnection details and the destination project. It uses `git` over SSH\nand `git-review` semantics to push to `refs/for/\u003cbranch\u003e` and relies on\nGerrit `Change-Id` trailers to create or update changes.\n\n## How it works (high level)\n\n- Discover pull request context and inputs.\n- **Detects PR operation mode** (CREATE, UPDATE, EDIT) based on event type.\n- Detects and prevents tool runs from creating duplicate changes.\n- Reads `.gitreview` for Gerrit host, port, and project.\n- When run locally, will pull `.gitreview` from the remote repository.\n- Sets up `git` user config and SSH for Gerrit.\n- **For UPDATE operations**: Finds and reuses existing Gerrit Change-IDs.\n- Prepare commits:\n  - one‑by‑one cherry‑pick with `Change-Id` trailers, or\n  - squash into a single commit and keep or reuse `Change-Id`.\n- Optionally replace the commit message with PR title and body.\n- Push with a topic to `refs/for/\u003cbranch\u003e` using `git-review` behavior.\n- **For UPDATE/EDIT operations**: Syncs PR metadata (title/description) to Gerrit.\n- Query Gerrit for the resulting URL, change number, and patchset SHA.\n- **Verifies patchset creation** to confirm updates vs. new changes.\n- Add a back‑reference comment in Gerrit to the GitHub PR and run URL.\n- Comment on the GitHub PR with the Gerrit change URL(s).\n- By default, the tool preserves PRs after submission; set `PRESERVE_GITHUB_PRS=false` to close them.\n\n## PR Update Handling (Dependabot Support)\n\nGitHub2Gerrit now **intelligently handles PR updates** from automation tools like Dependabot:\n\n### How PR Updates Work\n\nWhen a PR updates (e.g., Dependabot rebases or updates dependencies):\n\n1. **Automatic Detection**: The `synchronize` event triggers UPDATE mode\n2. **Change-ID Recovery**: Finds existing Gerrit change using four strategies:\n   - Topic-based query (`GH-owner-repo-PR#`)\n   - GitHub-Hash trailer matching\n   - GitHub-PR trailer URL matching\n   - Mapping comment parsing\n3. **Change-ID Reuse**: Forces reuse of existing Change-ID(s)\n4. **New Patchset Creation**: Pushes create a new patchset, not a new change\n5. **Metadata Sync**: Updates Gerrit change title/description if PR edits occur\n6. **Verification**: Confirms patchset creation and increment\n\n### PR Event Types\n\n| Event         | Action | Behavior                                  |\n| ------------- | ------ | ----------------------------------------- |\n| `opened`      | CREATE | Creates new Gerrit change(s)              |\n| `synchronize` | UPDATE | Updates existing change with new patchset |\n| `edited`      | EDIT   | Syncs metadata changes to Gerrit          |\n| `reopened`    | REOPEN | Treats as CREATE if no existing change    |\n| `closed`      | CLOSE  | Handles PR closure                        |\n\n### Example: Dependabot Workflow\n\n```yaml\non:\n  pull_request_target:\n    types: [opened, reopened, edited, synchronize, closed]\n```\n\n**Typical Dependabot flow:**\n\n1. **Day 1**: Dependabot opens PR #29 → GitHub2Gerrit creates Gerrit change 73940\n2. **Day 2**: Dependabot rebases PR #29 → GitHub2Gerrit updates change 73940 (new patchset 2)\n3. **Day 3**: Dependabot updates dependencies in PR #29 → change 73940 gets patchset 3\n4. **Day 4**: Someone edits PR title → metadata synced to Gerrit change 73940\n5. **Day 5**: Change 73940 merged in Gerrit → PR #29 auto-closed in GitHub\n\n### Key Features\n\n- **No Duplicate Changes**: UPDATE mode enforces existing change presence\n- **Robust Reconciliation**: Configurable similarity matching with dynamic threshold changes for PR updates\n- **Metadata Synchronization**: PR title/description changes sync to Gerrit\n- **Patchset Verification**: Confirms updates create new patchsets, not new changes\n- **Clear Error Messages**: Helpful guidance when existing change not found\n\n### Error Handling\n\nIf UPDATE fails to find existing change:\n\n```text\n❌ UPDATE FAILED: Cannot update non-existent Gerrit change\n💡 GitHub2Gerrit did not process PR #42.\n   To create a new change, trigger the 'opened' workflow action.\n```\n\n## PR Comment Commands\n\nGitHub2Gerrit supports an extensible set of directives issued through\npull request comments. Add a comment containing `@github2gerrit`\nfollowed by a command phrase and the tool will act on it during\nthe next workflow run.\n\n### Command Format\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n```text\n@github2gerrit \u003ccommand\u003e\n```\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n- Commands are **case-insensitive** — `@github2gerrit Create Missing Change`\n  works the same as `@github2gerrit create missing change`.\n- Only the **latest** occurrence of each command takes effect when the same\n  command appears in more than one comment.\n- The tool logs unrecognised directives at debug level and ignores them.\n\n### Available Commands\n\n\u003c!-- markdownlint-disable MD013 MD060 --\u003e\n\n| Command | Aliases | Description |\n| --- | --- | --- |\n| `create missing change` | `create-missing`, `create missing` | Create a Gerrit change when an UPDATE operation cannot find an existing one |\n\n\u003c!-- markdownlint-enable MD013 MD060 --\u003e\n\n### Create Missing Change\n\nWhen a PR `synchronize` event fires, GitHub2Gerrit treats it as an\n**UPDATE** operation and expects a Gerrit change to exist. If the\noriginal `opened` event failed (for example due to a bug or transient\nerror), no Gerrit change exists and every following update fails with:\n\n```text\n❌ UPDATE FAILED: Cannot update non-existent Gerrit change\n```\n\nThe **create missing change** command resolves this without manual\nintervention in Gerrit. Two mechanisms trigger it:\n\n#### 1. PR Comment Directive\n\nAdd a comment on the stuck pull request:\n\n```text\n@github2gerrit create missing change\n```\n\nThen re-trigger the workflow (push a trivial change or re-run the\nworkflow manually). GitHub2Gerrit detects the directive, switches\nfrom UPDATE to CREATE mode, and pushes a new Gerrit change.\n\n#### 2. CLI Flag\n\nOutside GitHub Actions you can pass the flag directly:\n\n```shell\ngithub2gerrit \\\n  --create-missing \\\n  https://github.com/MyOrg/my-repo/pull/42\n```\n\nOr set the environment variable:\n\n```shell\nexport CREATE_MISSING=true\ngithub2gerrit https://github.com/MyOrg/my-repo/pull/42\n```\n\n#### What Happens During Fallback\n\n1. The tool attempts the normal UPDATE flow and finds no existing\n   Gerrit change.\n2. It checks for `--create-missing` **or** scans PR comments for the\n   `@github2gerrit create missing change` directive.\n3. If authorised, the operation mode switches from UPDATE to CREATE.\n4. The tool posts a notice on the PR:\n\n   ```text\n   🔄 GitHub2Gerrit: No existing Gerrit change found for this PR.\n   Creating a new Gerrit change (fallback from UPDATE operation).\n   ```\n\n5. The pipeline continues as a normal CREATE — preparing commits,\n   pushing to Gerrit, posting the change URL back on the PR.\n\n#### GitHub Actions Workflow Example\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n```yaml\n- name: Submit PR to Gerrit\n  uses: lfreleng-actions/github2gerrit-action@main\n  with:\n    GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n    CREATE_MISSING: \"true\"   # always allow fallback\n```\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n\u003e **Tip:** Setting `CREATE_MISSING` to `true` in your workflow means\n\u003e stuck PRs self-heal on the next `synchronize` event without requiring\n\u003e a comment directive.\n\n## Close Merged PRs Feature\n\nGitHub2Gerrit now includes **automatic PR closure** when Gerrit merges changes\nand syncs them back to GitHub. This completes the lifecycle for automation PRs\n(like Dependabot).\n\n**How it works:**\n\n1. A bot (e.g., Dependabot) creates a GitHub PR\n2. GitHub2Gerrit converts it to a Gerrit change with tracking information\n3. When the Gerrit change is **merged** and synced to GitHub, the original PR is automatically closed\n4. When the Gerrit change is **abandoned**, the tool handles the PR based on `CLOSE_MERGED_PRS`:\n   - If `CLOSE_MERGED_PRS=true` (default): The tool closes the PR with an abandoned comment ⛔️\n   - If `CLOSE_MERGED_PRS=false`: PR remains open, but receives an abandoned notification comment ⛔️\n\n**Key characteristics:**\n\n- **Enabled by default** via `CLOSE_MERGED_PRS=true`\n- **Non-fatal operation** - the tool logs missing or already-closed PRs as\n  info, not errors\n- Works on `push` events when Gerrit syncs changes to GitHub mirrors\n- **Abandoned change handling**: The tool closes PRs or adds comments based on the `CLOSE_MERGED_PRS` setting\n\n**Gerrit change status handling:**\n\n\u003c!-- markdownlint-disable MD013 MD060 --\u003e\n\n| Scenario                    | `CLOSE_MERGED_PRS=true` (default)      | `CLOSE_MERGED_PRS=false`                               |\n| --------------------------- | -------------------------------------- | ------------------------------------------------------ |\n| Change has MERGED status    | ✅ Closes PR with merged comment       | ⏭️ No action                                           |\n| Change has ABANDONED status | ✅ Closes PR with abandoned comment ⛔️ | 💬 Adds abandoned notification comment (PR stays open) |\n| Change is NEW/OPEN          | ⚠️ Closes PR with a warning            | ⏭️ No action                                           |\n| Status UNKNOWN              | ⚠️ Closes PR with a warning            | ⏭️ No action                                           |\n\n\u003c!-- markdownlint-enable MD013 MD060 --\u003e\n\n**Status reporting examples:**\n\n```text\nNo GitHub PR URL found in commit abc123de - skipping\nGitHub PR #42 is already closed - nothing to do\nGerrit change confirmed as MERGED\nSUCCESS: Closed GitHub PR #42\n```\n\n**Abandoned change examples:**\n\nWith `CLOSE_MERGED_PRS=true`:\n\n```text\nGerrit change ABANDONED; will close PR with abandoned comment\nSUCCESS: Closed GitHub PR #42\n```\n\nWith `CLOSE_MERGED_PRS=false`:\n\n```text\nGerrit change ABANDONED; will add comment (CLOSE_MERGED_PRS=false)\nSUCCESS: Added comment to PR #42 (PR remains open)\n```\n\n## Automatic Cleanup Features\n\nGitHub2Gerrit includes **automatic cleanup operations** that run after successful\nPR processing or when you close PRs. These features help maintain synchronization\nbetween GitHub and Gerrit by cleaning up orphaned or stale changes.\n\n### Cleanup Operations\n\nThere are two cleanup operations that run automatically:\n\n#### 1. CLEANUP_ABANDONED (Abandoned Gerrit Changes → Close GitHub PRs)\n\n**What it does:** Scans all open GitHub PRs in the repository and closes those\nwhose corresponding Gerrit changes Gerrit has abandoned.\n\n- **Default:** ✅ Enabled (`CLEANUP_ABANDONED: true`)\n- **Configurable:** Set `CLEANUP_ABANDONED: false` in workflow to disable\n- **Runs during:** After successful PR processing, push events, and when you close PRs\n- **Behavior:**\n  - Finds open GitHub PRs with `GitHub-PR` trailers in their associated Gerrit changes\n  - Checks if the Gerrit change has `ABANDONED` status\n  - Closes the GitHub PR with an appropriate comment explaining the abandonment\n  - Respects the `CLOSE_MERGED_PRS` setting for whether to close or just comment\n\n**Example log output:**\n\n```text\nRunning abandoned PR cleanup...\nFound 150 open PRs to check\nPR #42 Gerrit change has ABANDONED status - will close\nAbandoned PR cleanup complete: closed 1 PR(s)\n```\n\n#### 2. CLEANUP_GERRIT (Closed GitHub PRs → Abandon Gerrit Changes)\n\n**What it does:** Scans all open Gerrit changes in the project and abandons those\nwhose corresponding GitHub PRs you have closed.\n\n- **Default:** ✅ Enabled (`CLEANUP_GERRIT: true`)\n- **Configurable:** Set `CLEANUP_GERRIT: false` in workflow to disable\n- **Runs during:** After successful PR processing, push events, and when you close PRs\n- **Behavior:**\n  - Queries all open Gerrit changes in the project\n  - Extracts the `GitHub-PR` trailer from each change\n  - Checks if the GitHub PR has closed status\n  - Abandons the Gerrit change with a message including:\n    - PR number and URL\n    - Any comments made when closing the PR\n    - Automatic attribution to GitHub2Gerrit\n\n**Example abandon message in Gerrit:**\n\n```text\nUser closed GitHub pull request #34\n\nPR URL: https://github.com/org/repo/pull/34\n\nComments when closing:\n\n--- Comment 1 ---\nComment by username:\nThis PR is no longer needed because...\n---\n\nGitHub2Gerrit automatically abandoned this change\nbecause user closed the source pull request.\n```\n\n**Example log output:**\n\n```text\nRunning Gerrit cleanup for closed GitHub PRs...\nScanning open Gerrit changes in project-name for closed GitHub PRs\nFound 25 open Gerrit change(s) to check\nGitHub PR #34 has closed status, will abandon Gerrit change 12345\nAbandoned Gerrit change 12345: https://gerrit.example.com/c/project/+/12345\nGerrit cleanup complete: abandoned 1 change(s)\n```\n\n### When Cleanup Runs\n\nCleanup operations run automatically in the following scenarios:\n\n1. **After successful PR processing** - When you open, synchronize, or edit a PR\n2. **On push events** - When Gerrit syncs changes back to GitHub (with `CLOSE_MERGED_PRS` enabled)\n3. **When you close PRs** - Immediately when the system detects a PR close event\n\n### PR Close Event Handling\n\nWhen you close a GitHub PR, GitHub2Gerrit performs the following actions in order:\n\n1. **Abandon the specific Gerrit change** for the closed PR\n   - Searches for the Gerrit change with matching `GitHub-PR` trailer\n   - Captures the last 3 comments from the PR (to preserve closure context)\n   - Abandons the Gerrit change with those comments included\n\n2. **Run CLEANUP_ABANDONED** - Close any other GitHub PRs with abandoned Gerrit changes\n\n3. **Run CLEANUP_GERRIT** - Abandon any other Gerrit changes with closed GitHub PRs\n\n**Example workflow:**\n\n```text\n🚪 PR closed event - running cleanup operations\nChecking for Gerrit change to abandon for PR #34\nFound Gerrit change 12345 for PR #34\n✅ Abandoned Gerrit change 12345\nRunning abandoned PR cleanup...\nRunning Gerrit cleanup for closed GitHub PRs...\n✅ Cleanup operations completed for closed PR\n```\n\n### Configuration\n\nThese cleanup operations **enable by default** but you can control them via\nworkflow inputs. They remain non-fatal, meaning if cleanup fails, the system logs a warning\nbut doesn't fail the entire workflow.\n\n**Configuration options:**\n\n- `CLEANUP_ABANDONED` - Default: `true` (shows ☑️ when enabled in configuration output)\n- `CLEANUP_GERRIT` - Default: `true` (shows ☑️ when enabled in configuration output)\n\n**Example - Disable cleanup operations:**\n\n```yaml\nuses: lfreleng-actions/github2gerrit-action@main\nwith:\n  GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n  CLEANUP_ABANDONED: false  # Don't close GitHub PRs for abandoned changes\n  CLEANUP_GERRIT: false     # Don't abandon Gerrit changes for closed PRs\n```\n\n**Example - Enable only one cleanup operation:**\n\n```yaml\nuses: lfreleng-actions/github2gerrit-action@main\nwith:\n  GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n  CLEANUP_ABANDONED: true   # Close GitHub PRs when Gerrit abandons changes\n  CLEANUP_GERRIT: false     # But don't abandon Gerrit changes when PRs close\n```\n\n**Dry-run support:** Both cleanup operations respect the `DRY_RUN` setting for testing.\n\n### Notes\n\n- Cleanup operations remain **parallel-safe** - workflow runs won't interfere with each other\n- Operations remain **idempotent** - safe to run repeatedly\n- The system skips PRs you already closed or changes Gerrit already abandoned (no duplicate actions)\n- The system logs errors during cleanup as warnings and doesn't fail the workflow\n\n## Restrict PRs to Automation Tools\n\nGitHub2Gerrit can restrict pull request processing to known automation tools.\nUse this for GitHub mirrors where you want contributors to submit changes via\nGerrit, while still accepting automated dependency updates from tools like\nDependabot.\n\n**Configuration:**\n\nSet `AUTOMATION_ONLY=true` (default) to enable, or `AUTOMATION_ONLY=false`\nto accept all PRs.\n\n**Recognized automation tools:**\n\n| Tool          | GitHub Username(s)                    |\n| ------------- | ------------------------------------- |\n| Dependabot    | `dependabot[bot]`, `dependabot`       |\n| Pre-commit.ci | `pre-commit-ci[bot]`, `pre-commit-ci` |\n\n**What happens when enabled:**\n\nThe tool rejects PRs from non-automation users by:\n\n1. Logging a warning message\n2. Closing the PR with this comment:\n\n   ```text\n   This GitHub mirror does not accept pull requests.\n   Please submit changes to the project's Gerrit server.\n   ```\n\n3. Exiting with code 1\n\n**Example:**\n\n```yaml\n- uses: lfit/github2gerrit-action@main\n  with:\n    AUTOMATION_ONLY: \"true\"  # default, accepts automation PRs\n    GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY }}\n```\n\n## Requirements\n\n- Repository contains a `.gitreview` file. If you cannot provide it,\n  you must pass `GERRIT_SERVER`, `GERRIT_SERVER_PORT`, and\n  `GERRIT_PROJECT` via the reusable workflow interface.\n- SSH key used to push changes into Gerrit\n- The system populates Gerrit known hosts automatically on first run.\n- The default `GITHUB_TOKEN` is available for PR metadata and comments.\n- The workflow grants permissions required for PR interactions:\n  - `pull-requests: write` (to comment on and close PRs)\n  - `issues: write` (to create PR comments via the Issues API)\n- The workflow runs with `pull_request_target` or via\n  `workflow_dispatch` using a valid PR context.\n\n## Error Codes\n\nThe `github2gerrit` tool uses standardized exit codes for different failure types. This helps with automation,\ndebugging, and providing clear feedback to users.\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n| Exit Code | Description             | Common Causes                                | Resolution                                              |\n| --------- | ----------------------- | -------------------------------------------- | ------------------------------------------------------- |\n| **0**     | Success                 | Operation completed                          | N/A                                                     |\n| **1**     | General Error           | Unexpected operational failure               | Check logs for details                                  |\n| **2**     | Configuration Error     | Missing or invalid configuration parameters  | Verify required inputs and environment variables        |\n| **3**     | Duplicate Error         | Duplicate change detected (when not allowed) | Use `--allow-duplicates` flag or check existing changes |\n| **4**     | GitHub API Error        | GitHub API access or permission issues       | Verify `GITHUB_TOKEN` has required permissions          |\n| **5**     | Gerrit Connection Error | Failed to connect to Gerrit server           | Check SSH keys, server configuration, and network       |\n| **6**     | Network Error           | Network connectivity issues                  | Check internet connection and firewall settings         |\n| **7**     | Repository Error        | Git repository access or operation failed    | Verify repository permissions and git configuration     |\n| **8**     | PR State Error          | Pull request in invalid state for processing | Ensure PR is open and mergeable                         |\n| **9**     | Validation Error        | Input validation failed                      | Check parameter values and formats                      |\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n### Common Error Messages\n\n#### GitHub API Permission Issues (Exit Code 4)\n\n```text\n❌ GitHub API query failed; provide a GITHUB_TOKEN with the required permissions\n```\n\n**Common causes:**\n\n- Missing `GITHUB_TOKEN` environment variable\n- Token lacks permissions for target repository\n- Token expired or invalid\n- Cross-repository access without proper token\n\n**Resolution:**\n\n- Configure `GITHUB_TOKEN` with a valid personal access token\n- For cross-repository workflows, use a token with access to the target repository\n- Grant required permissions: `contents: read`, `pull-requests: write`, `issues: write`\n\n#### Configuration Issues (Exit Code 2)\n\n```text\n❌ Configuration validation failed; check required parameters\n```\n\n**Common causes:**\n\n- Missing or invalid configuration parameters\n- Invalid parameter combinations\n- Missing `.gitreview` file without override parameters\n\n**Resolution:**\n\n- Verify all required inputs exist\n- Check parameter compatibility (e.g., don't use conflicting options)\n- Provide `GERRIT_SERVER`, `GERRIT_PROJECT` if `.gitreview` is missing\n\n#### Gerrit Connection Issues (Exit Code 5)\n\n```text\n❌ Gerrit connection failed; check SSH keys and server configuration\n```\n\n**Common causes:**\n\n- Invalid SSH private key\n- SSH key not added to Gerrit account\n- Incorrect Gerrit server configuration\n- Network connectivity to Gerrit server\n\n**Resolution:**\n\n- Verify SSH private key is correct and has access to Gerrit\n- Check Gerrit server hostname and port\n- Ensure network connectivity to Gerrit server\n\n### Integration Test Scenarios\n\nThe improved error handling is important for integration tests that run across different repositories.\nFor example, when testing the `github2gerrit-action` repository but accessing PRs in the `lfit/sandbox`\nrepository, you need:\n\n1. **Cross-Repository Token Access**: Use `READ_ONLY_GITHUB_TOKEN` instead of the default `GITHUB_TOKEN`\n   for workflows that access PRs in different repositories.\n\n2. **Clear Error Messages**: If the token lacks permissions, you'll see:\n\n   ```text\n   ❌ GitHub API query failed; provide a GITHUB_TOKEN with the required permissions\n   Details: Cannot access repository 'lfit/sandbox' - check token permissions\n   ```\n\n3. **Actionable Resolution**: The error message tells you what's needed - configure a token with access\n   to the target repository.\n\n### Debugging Workflow\n\nWhen troubleshooting failures:\n\n1. **Check the Exit Code**: Each failure has a unique exit code to help identify the root cause\n2. **Read the Error Message**: Look for the ❌ prefixed message that explains what went wrong\n3. **Review Details**: Context appears when available\n4. **Check Logs**: Enable verbose logging with `G2G_VERBOSE=true` for detailed debugging information\n\n### Note on sitecustomize.py\n\nThis repository includes a sitecustomize.py that is automatically\nimported by Python’s site initialization. It exists to make pytest and\ncoverage runs in CI more robust by:\n\n- assigns a unique COVERAGE_FILE per process to avoid mixing data across runs\n- proactively removing stale .coverage artifacts in common base directories.\n\nThe logic runs during pytest sessions and is best effort.\nIt never interferes with normal execution. Maintainers can keep it to\nstabilize coverage reporting for parallel/xdist runs.\n\n## Duplicate detection\n\nDuplicate detection uses a scoring-based approach. Instead of relying on a hash\nadded by this action, the detector compares the first line of the commit message\n(subject/PR title), analyzes the body text and the set of files changed, and\ncomputes a similarity score. When the score meets or exceeds a configurable\nthreshold (default 0.8), the tool treats the change as a duplicate and blocks\nsubmission. This approach aims to remain robust even when similar changes\nappeared outside this pipeline.\n\n### Examples of detected duplicates\n\n- Dependency bumps for the same package across close versions\n  (e.g., \"Bump foo from 1.0 to 1.1\" vs \"Bump foo from 1.1 to 1.2\")\n  with overlapping files — high score\n- Pre-commit autoupdates that change .pre-commit-config.yaml and hook versions —\n  high score\n- GitHub Actions version bumps that update .github/workflows/* uses lines —\n  medium to high score\n- Similar bug fixes with the same subject and significant file overlap —\n  strong match\n\n### Allowing duplicates\n\nUse `--allow-duplicates` or set `ALLOW_DUPLICATES=true` to override:\n\n```bash\n# CLI usage\ngithub2gerrit --allow-duplicates https://github.com/org/repo\n\n# GitHub Actions\nuses: lfreleng-actions/github2gerrit-action@main\nwith:\n  ALLOW_DUPLICATES: 'true'\n```\n\nWhen allowed, duplicates generate warnings but processing continues.\nThe tool exits with code 3 when it detects duplicates and they are not allowed.\n\n### Configuring duplicate detection scope\n\nBy default, the duplicate detector considers changes with status `open` when searching for potential duplicates.\nYou can customize which Gerrit change states to check using `--duplicate-types` or setting `DUPLICATE_TYPES`:\n\n```bash\n# CLI usage - check against open and merged changes\ngithub2gerrit --duplicate-types=open,merged https://github.com/org/repo\n\n# Environment variable\nDUPLICATE_TYPES=open,merged,abandoned github2gerrit https://github.com/org/repo\n\n# GitHub Actions\nuses: lfreleng-actions/github2gerrit-action@main\nwith:\n  DUPLICATE_TYPES: 'open,merged'\n```\n\nValid change states include `open`, `merged`, and `abandoned`. This setting determines which existing changes\nto check when evaluating whether a new change would be a duplicate.\n\n## Commit Message Normalization\n\nThe tool includes intelligent commit message normalization that automatically\nconverts automated PR titles (from tools like Dependabot, pre-commit.ci, etc.)\nto follow conventional commit standards. This feature defaults to enabled\nand you can control it via the `NORMALISE_COMMIT` setting.\n\n### How it works\n\n1. **Repository Analysis**: The tool analyzes your repository to determine\n   preferred conventional commit patterns by examining:\n   - `.pre-commit-config.yaml` for commit message formats\n   - `.github/release-drafter.yml` for commit type patterns\n   - Recent git history for existing conventional commit usage\n\n2. **Smart Detection**: Applies normalization to automated PRs from\n   known bots (dependabot[bot], pre-commit-ci[bot], etc.) or PRs with\n   automation patterns in the title.\n\n3. **Adaptive Formatting**: Respects your repository's existing conventions:\n   - **Capitalization**: Detects whether you use `feat:` or `FEAT:`\n   - **Commit Types**: Uses appropriate types (`chore`, `build`, `ci`, etc.)\n   - **Dependency Updates**: Converts \"Bump package from X to Y\" to\n     \"chore: bump package from X to Y\"\n\n### Examples\n\n**Before normalization:**\n\n```text\nBump net.logstash.logback:logstash-logback-encoder from 7.4 to 8.1\npre-commit autoupdate\nUpdate GitHub Action dependencies\n```\n\n**After normalization:**\n\n```text\nchore: bump net.logstash.logback:logstash-logback-encoder from 7.4 to 8.1\nchore: pre-commit autoupdate\nbuild: update GitHub Action dependencies\n```\n\n### Configuration\n\nEnable or disable commit normalization:\n\n```bash\n# CLI usage\ngithub2gerrit --normalise-commit https://github.com/org/repo\ngithub2gerrit --no-normalise-commit https://github.com/org/repo\n\n# Environment variable\nNORMALISE_COMMIT=true github2gerrit https://github.com/org/repo\nNORMALISE_COMMIT=false github2gerrit https://github.com/org/repo\n\n# GitHub Actions\nuses: lfreleng-actions/github2gerrit-action@main\nwith:\n  NORMALISE_COMMIT: 'true'  # default\n  # or\n  NORMALISE_COMMIT: 'false'  # disable\n```\n\n### Repository-specific Configuration\n\nTo influence the normalization behavior, configure your repository:\n\n**`.pre-commit-config.yaml`:**\n\n```yaml\nci:\n  autofix_commit_msg: |\n    Chore: pre-commit autofixes\n\n    Signed-off-by: pre-commit-ci[bot] \u003cpre-commit-ci@users.noreply.github.com\u003e\n  autoupdate_commit_msg: |\n    Chore: pre-commit autoupdate\n\n    Signed-off-by: pre-commit-ci[bot] \u003cpre-commit-ci@users.noreply.github.com\u003e\n```\n\n**`.github/release-drafter.yml`:**\n\n```yaml\nautolabeler:\n  - label: \"chore\"\n    title:\n      - \"/chore:/i\"\n  - label: \"feature\"\n    title:\n      - \"/feat:/i\"\n  - label: \"bug\"\n    title:\n      - \"/fix:/i\"\n```\n\nThe tool will detect the capitalization style from these files and apply\nit consistently to normalized commit messages.\n\n### Example Usage in CI/CD\n\n```bash\n# Run the tool and handle different exit codes\nif github2gerrit \"$PR_URL\"; then\n    echo \"✅ Submitted to Gerrit\"\nelif [ $? -eq 2 ]; then\n    echo \"❌ Configuration error - check your settings\"\n    exit 1\nelif [ $? -eq 3 ]; then\n    echo \"⚠️  Duplicate detected - use ALLOW_DUPLICATES=true to override\"\n    exit 0  # Treat as non-fatal in some workflows\nelse\n    echo \"❌ Runtime failure - check logs for details\"\n    exit 1\nfi\n```\n\n## Change-ID Reconciliation\n\nThe action includes an intelligent reconciliation system that reuses existing\nGerrit Change-IDs when updating pull requests. This prevents creating\nduplicate changes in Gerrit when developers rebase, add commits, or amend a PR.\n\n### How It Works\n\nWhen developers update a PR (e.g., via `synchronize` event), the reconciliation system:\n\n1. **Queries existing Gerrit changes** using the PR's topic (or falls back to GitHub comments)\n2. **Matches local commits** to existing changes using these strategies:\n   - **Trailer matching**: Reuses Change-IDs already present in commit messages\n   - **Exact subject matching**: Matches commits with identical subjects\n   - **File signature matching**: Matches commits with identical file changes\n   - **Subject similarity matching**: Uses Jaccard similarity on commit subjects\n3. **Generates new Change-IDs** for commits that don't match any existing change\n\n### Configuration\n\nThe reconciliation behavior can be fine-tuned with these parameters:\n\n**`REUSE_STRATEGY`** (default: `topic+comment`)\n\n- `topic`: Query Gerrit changes by topic\n- `comment`: Search GitHub PR comments for Change-IDs\n- `topic+comment`: Try topic first, fall back to comments\n- `none`: Disable reconciliation (always generate new Change-IDs)\n\n**`SIMILARITY_SUBJECT`** (default: `0.7`)\n\n- Jaccard similarity threshold (0.0-1.0) for subject matching\n- Higher values require more similarity between commit subjects\n- Example: `0.7` means 70% of words must match\n\n**`SIMILARITY_UPDATE_FACTOR`** (default: `0.75`)\n\n- Multiplier applied to similarity threshold for UPDATE operations\n- Allows more lenient matching for rebased/amended commits\n- Applied as: `update_threshold = max(0.5, base_threshold × factor)`\n- Example: With base `0.7` and factor `0.75`, UPDATE threshold becomes `0.525`\n- Floor threshold of `0.5` prevents too-loose matching\n\n**`SIMILARITY_FILES`** (default: `false`)\n\n- Whether to require exact file signature match during reconciliation (Pass C)\n- When `true`: Commits must touch the exact same set of files to match (strict mode)\n- When `false` (recommended): Skips file signature matching, relies on subject matching\n- **Why default is `false`**: File signature matching is too strict for common workflows:\n  - Developers add/remove files during PR updates\n  - Rebasing shifts file changes between commits\n  - Conflict resolution changes which files a commit touches\n  - Developers amend commits with more file changes\n- **When to use `true`**: Enable this for controlled workflows where file sets never change\n\n**`ALLOW_ORPHAN_CHANGES`** (default: `false`)\n\n- When enabled, unmatched Gerrit changes don't generate warnings\n- Useful when you expect to remove changes from the topic\n\n### Why Adjustable Similarity?\n\nPR updates often involve rebasing, which can change commit messages slightly\n(e.g., updating references, fixing typos, or resolving conflicts). The\n`SIMILARITY_UPDATE_FACTOR` allows the system to recognize these as the same\nlogical change despite minor message differences:\n\n- **Base threshold** (`SIMILARITY_SUBJECT`): Used for initial PR creation\n- **Update threshold** (base × factor): Used for PR synchronize events\n- **Percentage-based**: Scales consistently across different base thresholds\n- **Floor at 0.5**: Prevents matching unrelated commits\n\n### Example Configurations\n\n```bash\n# Strict matching - require 90% similarity, minor relaxation on updates\nSIMILARITY_SUBJECT=0.9\nSIMILARITY_UPDATE_FACTOR=0.85\n\n# Lenient matching - allow more variation in commit messages\nSIMILARITY_SUBJECT=0.6\nSIMILARITY_UPDATE_FACTOR=0.7\n\n# Recommended: Flexible matching for most workflows (default settings)\nSIMILARITY_SUBJECT=0.7\nSIMILARITY_UPDATE_FACTOR=0.75\nSIMILARITY_FILES=false  # default - allows file changes in PR updates\n\n# Strict matching - use for controlled workflows\nSIMILARITY_SUBJECT=0.9\nSIMILARITY_UPDATE_FACTOR=0.85\nSIMILARITY_FILES=true  # requires exact file matches\n\n# Disable reconciliation (always create new Change-IDs)\nREUSE_STRATEGY=none\n```\n\n### Common Pitfalls\n\n**File signature matching can break reconciliation during normal workflows:**\n\n### GitHub Actions Example\n\n```yaml\n- uses: lfreleng-actions/github2gerrit-action@main\n  with:\n    GERRIT_KNOWN_HOSTS: ${{ secrets.GERRIT_KNOWN_HOSTS }}\n    GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n    SIMILARITY_SUBJECT: '0.75'\n    SIMILARITY_UPDATE_FACTOR: '0.8'\n    # SIMILARITY_FILES defaults to 'false' - uncomment to enable strict mode\n    # SIMILARITY_FILES: 'true'\n```\n\n### CLI Example\n\n```bash\n# Custom similarity settings\ngithub2gerrit \\\n  --similarity-subject 0.75 \\\n  --similarity-update-factor 0.8 \\\n  https://github.com/owner/repo/pull/123\n```\n\n## Usage\n\nThis action runs as part of a workflow that triggers on\n`pull_request_target` and also supports manual runs via\n`workflow_dispatch`.\n\nMinimal example:\n\n```yaml\nname: github2gerrit\n\non:\n  pull_request_target:\n    types: [opened, reopened, edited, synchronize, closed]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pull-requests: write\n  issues: write\n\njobs:\n  submit-to-gerrit:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Submit PR to Gerrit\n        id: g2g\n        uses: lfreleng-actions/github2gerrit-action@main\n        with:\n          SUBMIT_SINGLE_COMMITS: \"false\"\n          USE_PR_AS_COMMIT: \"false\"\n          FETCH_DEPTH: \"10\"\n          GERRIT_KNOWN_HOSTS: ${{ vars.GERRIT_KNOWN_HOSTS }}\n          GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n          GERRIT_SSH_USER_G2G: ${{ vars.GERRIT_SSH_USER_G2G }}\n          GERRIT_SSH_USER_G2G_EMAIL: ${{ vars.GERRIT_SSH_USER_G2G_EMAIL }}\n          ORGANIZATION: ${{ github.repository_owner }}\n          REVIEWERS_EMAIL: \"\"\n          ISSUE_ID: \"\"  # Optional: adds 'Issue-ID: ...' trailer to the commit message\n          ISSUE_ID_LOOKUP_JSON: ${{ vars.ISSUE_ID_LOOKUP_JSON }}  # Optional: JSON lookup table for automatic Issue-ID resolution\n```\n\nThe action reads `.gitreview`. If `.gitreview` is absent, you must\nsupply Gerrit connection details through a reusable workflow or by\nsetting the corresponding environment variables before invoking the\naction. The shell action enforces `.gitreview` for the composite\nvariant; this Python action mirrors that behavior for compatibility.\n\n## Command Line Usage and Debugging\n\n### Direct Command Line Usage\n\nYou can run the tool directly from the command line to process GitHub pull requests.\n\n**For development (with local checkout):**\n\n```bash\n# Process a specific pull request\nuv run github2gerrit https://github.com/owner/repo/pull/123\n\n# Process all open pull requests in a repository\nuv run github2gerrit https://github.com/owner/repo\n\n# Run in CI mode (reads from environment variables)\nuv run github2gerrit\n```\n\n**For CI/CD or one-time usage:**\n\n```bash\n# Install and run in one command\nuvx github2gerrit https://github.com/owner/repo/pull/123\n\n# Install from specific version/source\nuvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit https://github.com/owner/repo/pull/123\n```\n\n### Available Options\n\n```bash\n# View help (local development)\nuv run github2gerrit --help\n\n# View help (CI/CD)\nuvx github2gerrit --help\n```\n\nThe comprehensive [Inputs](#inputs) table above documents all CLI options and shows\nalignment between action inputs, environment variables, and CLI flags. All CLI flags\nhave corresponding environment variables for configuration.\n\nKey options include:\n\n- `--verbose` / `-v`: Enable verbose debug logging (`G2G_VERBOSE`)\n- `--dry-run`: Check configuration without making changes (`DRY_RUN`)\n- `--submit-single-commits`: Submit each commit individually (`SUBMIT_SINGLE_COMMITS`)\n- `--use-pr-as-commit`: Use PR title/body as commit message (`USE_PR_AS_COMMIT`)\n- `--issue-id`: Add an Issue-ID trailer (e.g., \"Issue-ID: ABC-123\") to the commit message (`ISSUE_ID`)\n- `--preserve-github-prs`: Don't close GitHub PRs after submission (`PRESERVE_GITHUB_PRS`)\n- `--duplicate-types`: Configure which Gerrit change states to check for duplicates (`DUPLICATE_TYPES`)\n\nFor a complete list of all available options, see the [Inputs](#inputs) section.\n\n### Debugging and Troubleshooting\n\nWhen encountering issues, enable verbose logging to see detailed execution:\n\n```bash\n# Using the CLI flag\ngithub2gerrit --verbose https://github.com/owner/repo/pull/123\n\n# Using environment variable\nG2G_LOG_LEVEL=DEBUG github2gerrit https://github.com/owner/repo/pull/123\n\n# Alternative environment variable\nG2G_VERBOSE=true github2gerrit https://github.com/owner/repo/pull/123\n```\n\nDebug output includes:\n\n- Git command execution and output\n- SSH connection attempts\n- Gerrit API interactions\n- Branch resolution logic\n- Change-Id processing\n\nCommon issues and solutions:\n\n1. **Configuration Validation Errors**: The tool provides clear error messages when\n   required configuration is missing or invalid. Look for messages starting with\n   \"Configuration validation failed:\" that specify missing inputs like\n   `GERRIT_KNOWN_HOSTS`, `GERRIT_SSH_PRIVKEY_G2G`, etc.\n\n2. **SSH Permission Denied**:\n   - Ensure `GERRIT_SSH_PRIVKEY_G2G` and `GERRIT_KNOWN_HOSTS` are properly set\n   - If you see \"Permissions 0644 for 'gerrit_key' are too open\", the action will automatically\n     try SSH agent authentication\n   - For persistent file permission issues, ensure `G2G_USE_SSH_AGENT=true` (default)\n\n3. **Branch Not Found**: Check that the target branch exists in both GitHub and Gerrit\n4. **Change-Id Issues**: Enable debug logging to see Change-Id generation and validation\n5. **Account Not Found Errors**: If you see \"Account '\u003cEmail@Domain.com\u003e' not found\",\n   ensure your Gerrit account email matches your git config email (case-sensitive).\n6. **Gerrit API Errors**: Verify Gerrit server connectivity and project permissions\n\n\u003e **Note**: The tool displays configuration errors cleanly without Python tracebacks.\n\u003e If you see a traceback in the output, please report it as a bug.\n\n### Environment Variables\n\nThe comprehensive [Inputs](#inputs) table above documents all environment variables.\nKey variables for CLI usage include:\n\n- `G2G_LOG_LEVEL`: Set to `DEBUG` for verbose output (default: `WARNING`)\n- `G2G_VERBOSE`: Set to `true` to enable debug logging (same as `--verbose` flag)\n- `GERRIT_SSH_PRIVKEY_G2G`: SSH private key content\n- `GERRIT_KNOWN_HOSTS`: SSH known hosts entries\n- `GERRIT_SSH_USER_G2G`: Gerrit SSH username\n- `G2G_USE_SSH_AGENT`: Set to `false` to force file-based SSH (default: `true`)\n- `DRY_RUN`: Set to `true` for check mode\n- `CI_TESTING`: Set to `true` to ignore `.gitreview` file and use environment variables instead\n\nFor a complete list of all supported environment variables, their defaults, and\ntheir corresponding action inputs and CLI flags, see the [Inputs](#inputs) section.\n\n## Advanced usage\n\n### Overriding .gitreview Settings\n\nWhen `CI_TESTING=true`, the tool ignores any `.gitreview` file in the\nrepository and uses environment variables instead. This is useful for:\n\n- **Integration testing** against different Gerrit servers\n- **Overriding repository settings** when the `.gitreview` points to the wrong server\n- **Development and debugging** with custom Gerrit configurations\n\n**Example:**\n\n```bash\nexport CI_TESTING=true\nexport GERRIT_SERVER=gerrit.example.org\nexport GERRIT_PROJECT=sandbox\ngithub2gerrit https://github.com/org/repo/pull/123\n```\n\n### SSH Authentication Methods\n\nThis action supports two SSH authentication methods:\n\n1. **SSH Agent Authentication (Default)**: More secure, avoids file permission issues in CI\n2. **File-based Authentication**: Fallback method that writes keys to temporary files\n\n#### SSH Agent Authentication\n\nBy default, the action uses SSH agent to load keys into memory rather than writing them to disk. This is more\nsecure and avoids the file permission issues commonly seen in CI environments.\n\nTo control this behavior:\n\n```yaml\n- name: Submit to Gerrit\n  uses: your-org/github2gerrit-action@v1\n  env:\n    G2G_USE_SSH_AGENT: \"true\"  # Default: enables SSH agent (recommended)\n    # G2G_USE_SSH_AGENT: \"false\"  # Forces file-based authentication\n  with:\n    GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n    # ... other inputs\n```\n\n**Benefits of SSH Agent Authentication:**\n\n- No temporary files written to disk\n- Avoids SSH key file permission issues (0644 vs 0600)\n- More secure in containerized CI environments\n- Automatic cleanup when process exits\n\n#### File-based Authentication (Fallback)\n\nIf SSH agent setup fails, the action automatically falls back to writing the SSH key to a temporary file with\nsecure permissions. This method:\n\n- Creates files in workspace-specific `.ssh-g2g/` directory\n- Attempts to set proper file permissions (0600)\n- Includes four fallback permission-setting strategies for CI environments\n\n### Custom SSH Configuration\n\nYou can explicitly install the SSH key and provide a custom SSH configuration\nbefore invoking this action. This is useful when:\n\n- You want to override the port/host used by SSH\n- You need to define host aliases or SSH options\n- Your Gerrit instance uses a non-standard HTTP base path (e.g. /r)\n\nExample:\n\n```yaml\nname: github2gerrit (advanced)\n\non:\n  pull_request_target:\n    types: [opened, reopened, edited, synchronize, closed]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pull-requests: write\n  issues: write\n\njobs:\n  submit-to-gerrit:\n    runs-on: ubuntu-latest\n    steps:\n\n\n      - name: Submit PR to Gerrit (with explicit overrides)\n        id: g2g\n        uses: lfreleng-actions/github2gerrit-action@main\n        with:\n          # Behavior\n          SUBMIT_SINGLE_COMMITS: \"false\"\n          USE_PR_AS_COMMIT: \"false\"\n          FETCH_DEPTH: \"10\"\n\n          # Required SSH/identity\n          GERRIT_KNOWN_HOSTS: ${{ vars.GERRIT_KNOWN_HOSTS }}\n          GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n          GERRIT_SSH_USER_G2G: ${{ vars.GERRIT_SSH_USER_G2G }}\n          GERRIT_SSH_USER_G2G_EMAIL: ${{ vars.GERRIT_SSH_USER_G2G_EMAIL }}\n\n          # Optional overrides when .gitreview is missing or to force values\n          GERRIT_SERVER: ${{ vars.GERRIT_SERVER }}\n          GERRIT_SERVER_PORT: ${{ vars.GERRIT_SERVER_PORT }}\n          GERRIT_PROJECT: ${{ vars.GERRIT_PROJECT }}\n\n          # Optional Gerrit REST base path and credentials (if required)\n          # e.g. '/r' for some deployments\n          GERRIT_HTTP_BASE_PATH: ${{ vars.GERRIT_HTTP_BASE_PATH }}\n          GERRIT_HTTP_USER: ${{ vars.GERRIT_HTTP_USER }}\n          GERRIT_HTTP_PASSWORD: ${{ secrets.GERRIT_HTTP_PASSWORD }}\n\n          ORGANIZATION: ${{ github.repository_owner }}\n          REVIEWERS_EMAIL: \"\"\n```\n\nNotes:\n\n- The action configures SSH internally using the provided inputs (key,\n  known_hosts) and does not use the runner’s SSH agent or ~/.ssh/config.\n- Do not add external steps to install SSH keys or edit SSH config; they’re\n  unnecessary and may conflict with the action.\n\n## GitHub Enterprise support\n\n- Direct-URL mode accepts enterprise GitHub hosts when explicitly enabled.\n  Default: off (use github.com by default). Enable via the CLI flag\n  --allow-ghe-urls or by setting ALLOW_GHE_URLS=\"true\".\n- In GitHub Actions, this action works with GitHub Enterprise when the\n  workflow runs in that enterprise environment and provides a valid\n  GITHUB_TOKEN. For direct-URL runs outside Actions, ensure ORGANIZATION\n  and GITHUB_REPOSITORY reflect the target repository.\n\n## Inputs\n\nAll inputs are strings, matching the composite action. The following table shows\nalignment between action inputs, environment variables, and CLI flags:\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n| Action Input                | Environment Variable        | CLI Flag                      | Required | Default                          | Description                                                                                |\n| --------------------------- | --------------------------- | ----------------------------- | -------- | -------------------------------- | ------------------------------------------------------------------------------------------ |\n| `SUBMIT_SINGLE_COMMITS`     | `SUBMIT_SINGLE_COMMITS`     | `--submit-single-commits`     | No       | `\"false\"`                        | Submit one commit at a time to Gerrit                                                      |\n| `USE_PR_AS_COMMIT`          | `USE_PR_AS_COMMIT`          | `--use-pr-as-commit`          | No       | `\"false\"`                        | Use PR title and body as the commit message                                                |\n| `FETCH_DEPTH`               | `FETCH_DEPTH`               | `--fetch-depth`               | No       | `\"10\"`                           | Fetch depth for checkout                                                                   |\n| `PR_NUMBER`                 | `PR_NUMBER`                 | N/A                           | No       | `\"0\"`                            | Pull request number to process (workflow_dispatch)                                         |\n| `GERRIT_KNOWN_HOSTS`        | `GERRIT_KNOWN_HOSTS`        | `--gerrit-known-hosts`        | Yes      | N/A                              | SSH known hosts entries for Gerrit                                                         |\n| `GERRIT_SSH_PRIVKEY_G2G`    | `GERRIT_SSH_PRIVKEY_G2G`    | `--gerrit-ssh-privkey-g2g`    | Yes      | N/A                              | SSH private key content for Gerrit authentication                                          |\n| `GERRIT_SSH_USER_G2G`       | `GERRIT_SSH_USER_G2G`       | `--gerrit-ssh-user-g2g`       | No¹      | `\"\"`                             | Gerrit SSH username (auto-derived if enabled)                                              |\n| `GERRIT_SSH_USER_G2G_EMAIL` | `GERRIT_SSH_USER_G2G_EMAIL` | `--gerrit-ssh-user-g2g-email` | No¹      | `\"\"`                             | Email for Gerrit SSH user (auto-derived if enabled)                                        |\n| `ORGANIZATION`              | `ORGANIZATION`              | `--organization`              | No       | `${{ github.repository_owner }}` | GitHub organization/owner                                                                  |\n| `REVIEWERS_EMAIL`           | `REVIEWERS_EMAIL`           | `--reviewers-email`           | No       | `\"\"`                             | Comma-separated reviewer emails                                                            |\n| `ALLOW_GHE_URLS`            | `ALLOW_GHE_URLS`            | `--allow-ghe-urls`            | No       | `\"false\"`                        | Allow GitHub Enterprise URLs in direct URL mode                                            |\n| `PRESERVE_GITHUB_PRS`       | `PRESERVE_GITHUB_PRS`       | `--preserve-github-prs`       | No       | `\"true\"`                         | Do not close GitHub PRs after pushing to Gerrit                                            |\n| `DRY_RUN`                   | `DRY_RUN`                   | `--dry-run`                   | No       | `\"false\"`                        | Check settings/PR metadata; do not write to Gerrit                                         |\n| `ALLOW_DUPLICATES`          | `ALLOW_DUPLICATES`          | `--allow-duplicates`          | No       | `\"false\"`                        | Allow submitting duplicate changes without error                                           |\n| `CI_TESTING`                | `CI_TESTING`                | `--ci-testing`                | No       | `\"false\"`                        | Enable CI testing mode (overrides .gitreview)                                              |\n| `ISSUE_ID`                  | `ISSUE_ID`                  | `--issue-id`                  | No       | `\"\"`                             | Issue ID to include (e.g., ABC-123)                                                        |\n| `ISSUE_ID_LOOKUP_JSON`      | `ISSUE_ID_LOOKUP_JSON`      | `--issue-id-lookup-json`      | No       | `\"[]\"`                           | JSON array mapping GitHub actors to Issue IDs (automatic lookup if ISSUE_ID not provided)  |\n| `G2G_USE_SSH_AGENT`         | `G2G_USE_SSH_AGENT`         | N/A                           | No       | `\"true\"`                         | Use SSH agent for authentication                                                           |\n| `DUPLICATE_TYPES`           | `DUPLICATE_TYPES`           | `--duplicate-types`           | No       | `\"open\"`                         | Comma-separated Gerrit change states to check for duplicate detection                      |\n| `AUTOMATION_ONLY`           | `AUTOMATION_ONLY`           | `--automation-only`           | No       | `\"true\"`                         | Accept PRs from automation tools only (dependabot, pre-commit-ci); reject human PRs        |\n| `CLEANUP_ABANDONED`         | `CLEANUP_ABANDONED`         | N/A                           | No       | `\"true\"`                         | Close GitHub PRs for abandoned Gerrit changes                                              |\n| `CLEANUP_GERRIT`            | `CLEANUP_GERRIT`            | N/A                           | No       | `\"true\"`                         | Abandon Gerrit changes for closed GitHub PRs closure                                       |\n| `REUSE_STRATEGY`            | `REUSE_STRATEGY`            | `--reuse-strategy`            | No       | `\"topic+comment\"`                | Change-ID reuse strategy: `topic`, `comment`, `topic+comment`, or `none`                   |\n| `SIMILARITY_SUBJECT`        | `SIMILARITY_SUBJECT`        | `--similarity-subject`        | No       | `\"0.7\"`                          | Jaccard similarity threshold (0.0-1.0) for subject matching during reconciliation          |\n| `SIMILARITY_UPDATE_FACTOR`  | `SIMILARITY_UPDATE_FACTOR`  | `--similarity-update-factor`  | No       | `\"0.75\"`                         | Multiplier (0.0-1.0) for similarity threshold on PR UPDATE operations (rebases/amendments) |\n| `SIMILARITY_FILES`          | `SIMILARITY_FILES`          | `--similarity-files`          | No       | `\"false\"`                        | Require exact file signature match for reconciliation (strict mode)                        |\n| `ALLOW_ORPHAN_CHANGES`      | `ALLOW_ORPHAN_CHANGES`      | `--allow-orphan-changes`      | No       | `\"false\"`                        | Keep unmatched Gerrit changes without warning during reconciliation                        |\n| `GERRIT_SERVER`             | `GERRIT_SERVER`             | `--gerrit-server`             | No²      | `\"\"`                             | Gerrit server hostname (auto-derived if enabled)                                           |\n| `GERRIT_SERVER_PORT`        | `GERRIT_SERVER_PORT`        | `--gerrit-server-port`        | No       | `\"29418\"`                        | Gerrit SSH port                                                                            |\n| `GERRIT_PROJECT`            | `GERRIT_PROJECT`            | `--gerrit-project`            | No²      | `\"\"`                             | Gerrit project name                                                                        |\n| `GERRIT_HTTP_BASE_PATH`     | `GERRIT_HTTP_BASE_PATH`     | N/A                           | No       | `\"\"`                             | HTTP base path for Gerrit REST API                                                         |\n| `GERRIT_HTTP_USER`          | `GERRIT_HTTP_USER`          | N/A                           | No       | `\"\"`                             | Gerrit HTTP user for REST authentication                                                   |\n| `GERRIT_HTTP_PASSWORD`      | `GERRIT_HTTP_PASSWORD`      | N/A                           | No       | `\"\"`                             | Gerrit HTTP password/token for REST authentication                                         |\n| N/A                         | `G2G_VERBOSE`               | `--verbose`, `-v`             | No       | `\"false\"`                        | Enable verbose debug logging                                                               |\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n**Notes:**\n\n1. Auto-derived when `G2G_ENABLE_DERIVATION=true` (default: true in all contexts)\n2. Optional if `.gitreview` file exists in repository\n\nThe format required for the JSON Issue-ID lookup is:\n\n`[{\"key\": \"username\", \"value\": \"ISSUE-ID\"}]`\n\n### Internal Environment Variables\n\nThe following environment variables control internal behavior but are not action inputs:\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n| Environment Variable         | Description                                    | Default                                    |\n| ---------------------------- | ---------------------------------------------- | ------------------------------------------ |\n| `G2G_LOG_LEVEL`              | Logging level (DEBUG, INFO, WARNING, ERROR)    | `\"WARNING\"`                                |\n| `G2G_ENABLE_DERIVATION`      | Enable auto-derivation of Gerrit parameters    | `\"true\"`                                   |\n| `G2G_CONFIG_PATH`            | Path to organization configuration file        | `~/.config/github2gerrit/config.ini`       |\n| `G2G_AUTO_SAVE_CONFIG`       | Auto-save derived parameters to config         | `\"false\"` (GitHub Actions), `\"true\"` (CLI) |\n| `G2G_TARGET_URL`             | Internal flag for direct URL mode              | Set automatically                          |\n| `G2G_TMP_BRANCH`             | Temporary branch name for single commits       | `\"tmp_branch\"`                             |\n| `G2G_TOPIC_PREFIX`           | Prefix for Gerrit topic names                  | `\"GH\"`                                     |\n| `G2G_SKIP_GERRIT_COMMENTS`   | Skip posting back-reference comments in Gerrit | `\"false\"`                                  |\n| `G2G_DRYRUN_DISABLE_NETWORK` | Disable network calls in dry-run mode          | `\"false\"`                                  |\n| `SYNC_ALL_OPEN_PRS`          | Process all open PRs (set automatically)       | Set automatically                          |\n| `GERRIT_BRANCH`              | Override target branch for Gerrit              | Uses `GITHUB_BASE_REF`                     |\n| `GITHUB_TOKEN`               | GitHub API token                               | Provided by GitHub Actions                 |\n| `GITHUB_*` context           | GitHub Actions context variables               | Provided by GitHub Actions                 |\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n## Outputs\n\nThe action provides the following outputs for use in later workflow steps:\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n\n| Output Name                 | Description                                 | Environment Variable        |\n| --------------------------- | ------------------------------------------- | --------------------------- |\n| `gerrit_change_request_url` | Gerrit change URL(s) (newline-separated)    | `GERRIT_CHANGE_REQUEST_URL` |\n| `gerrit_change_request_num` | Gerrit change number(s) (newline-separated) | `GERRIT_CHANGE_REQUEST_NUM` |\n| `gerrit_commit_sha`         | Patch set commit SHA(s) (newline-separated) | `GERRIT_COMMIT_SHA`         |\n\n\u003c!-- markdownlint-enable MD013 --\u003e\n\nThese outputs export automatically as environment variables and are accessible in\nlater workflow steps using `${{ steps.\u003cstep-id\u003e.outputs.\u003coutput-name\u003e }}` syntax.\n\n## Configuration and Parameters\n\nFor a complete list of all supported configuration parameters, including action\ninputs, environment variables, and CLI flags, see the comprehensive [Inputs](#inputs)\ntable above.\n\n### Configuration Precedence\n\nThe tool follows this precedence order for configuration values:\n\n1. **CLI flags** (highest priority)\n2. **Environment variables**\n3. **Configuration file values**\n4. **Tool defaults** (lowest priority)\n\n### Configuration File Format\n\nConfiguration files use INI format with organization-specific sections:\n\n```ini\n[default]\nGERRIT_SERVER = \"gerrit.example.org\"\nPRESERVE_GITHUB_PRS = \"true\"\n\n[onap]\nISSUE_ID = \"CIMAN-33\"\nREVIEWERS_EMAIL = \"user@example.org\"\n\n[opendaylight]\nGERRIT_HTTP_USER = \"bot-user\"\nGERRIT_HTTP_PASSWORD = \"${ENV:ODL_GERRIT_TOKEN}\"\n```\n\n### Using .netrc Files\n\nGitHub2Gerrit supports loading Gerrit credentials from `.netrc` files, following\nthe standard format used by curl and other tools.\n\n**Search order:**\n\n1. `.netrc` in the current directory\n2. `~/.netrc` in your home directory\n3. `~/_netrc` (Windows fallback)\n\n**Example `.netrc` file:**\n\n```text\nmachine gerrit.onap.org login myuser password mytoken\nmachine gerrit.opendaylight.org login myuser password anothertoken\n```\n\n**CLI options:**\n\n| Option | Description |\n| ------ | ----------- |\n| `--no-netrc` | Disable .netrc file lookup |\n| `--netrc-file PATH` | Use a specific .netrc file |\n| `--netrc-optional` | Do not fail if .netrc file is missing (default) |\n| `--netrc-required` | Require a .netrc file and fail if missing |\n\nBy default, `.netrc` lookup is optional (`--netrc-optional`): if the tool\nfinds no `.netrc` file, it continues and falls back to environment variables.\nUse `--netrc-required` to enforce that a `.netrc` file must be present.\n\nWhen a `.netrc` file is present, the tool loads credentials automatically.\nExplicit environment variables or CLI arguments take precedence over `.netrc`\nentries.\n\n**Credential Priority Order:**\n\n1. **CLI arguments** (highest priority)\n2. **`.netrc` file** (if not disabled with `--no-netrc`)\n3. **Environment variables** (e.g., `GERRIT_HTTP_USER`, `GERRIT_HTTP_PASSWORD`)\n\nThe tool loads configuration from `~/.config/github2gerrit/configuration.txt`\nby default, or from the path specified in the `G2G_CONFIG_PATH` environment\nvariable.\n\n**Note**: Unknown configuration keys will generate warnings to help catch typos\nand missing functionality.\n\n### Credential Derivation\n\nWhen `GERRIT_SSH_USER_G2G` and `GERRIT_SSH_USER_G2G_EMAIL` are not explicitly provided,\nthe tool automatically derives these credentials using a multi-source approach with the\nfollowing priority order:\n\n#### Derivation Sources (in priority order)\n\n1. **SSH Config User** (if `G2G_RESPECT_USER_SSH=true` in local mode)\n   - Reads from `~/.ssh/config` for the specific Gerrit host\n   - Matches host patterns (supports wildcards like `gerrit.*`)\n   - Extracts the `User` directive for matching entries\n\n2. **Git User Email** (if `G2G_RESPECT_USER_SSH=true` in local mode)\n   - Reads from local git configuration (`git config user.email`)\n   - Used as the email address for commits\n\n3. **Organization-based Fallback** (default for GitHub Actions)\n   - Derives credentials from the GitHub organization name\n   - Generates standardized values\n\n#### Organization-based Pattern\n\nThe fallback credentials follow this pattern based on the `ORGANIZATION` value:\n\n- **Gerrit Server**: Derived as `gerrit.{organization}.org` (or from config file)\n- **SSH Username**: `{organization}.gh2gerrit`\n- **Email Address**: `releng+{organization}-gh2gerrit@linuxfoundation.org`\n\n**Example**: For organization `onap`:\n\n- Server: `gerrit.onap.org`\n- Username: `onap.gh2gerrit`\n- Email: `releng+onap-gh2gerrit@linuxfoundation.org`\n\n#### Organization Name Source\n\nThe tool determines the organization name from GitHub context in the following order:\n\n1. Explicit `ORGANIZATION` parameter (action input or environment variable)\n2. `GITHUB_REPOSITORY_OWNER` (automatically set by GitHub Actions to the repository owner)\n\n**Example**: For a repository `onap/releng-builder`:\n\n- Organization: `onap` (from `github.repository_owner`)\n- Derived server: `gerrit.onap.org`\n- Derived username: `onap.gh2gerrit`\n- Derived email: `releng+onap-gh2gerrit@linuxfoundation.org`\n\nThe tool normalizes the organization name to lowercase before using it to construct the\nGerrit server hostname and credentials.\n\n#### Local Development Mode\n\nFor local CLI usage, set `G2G_RESPECT_USER_SSH=true` to use your personal SSH config\nand git config instead of organization-based defaults:\n\n```bash\n# Enable personalized credentials from SSH/git config\nexport G2G_RESPECT_USER_SSH=true\ngithub2gerrit https://github.com/org/repo/pull/123\n```\n\n**Example `~/.ssh/config` entry:**\n\n```ssh-config\nHost gerrit.*.org\n    User alice\n\nHost gerrit.opendaylight.org\n    User alice-odl\n    Port 29418\n```\n\nWith this configuration and `G2G_RESPECT_USER_SSH=true`:\n\n- Username will be `alice` (from SSH config)\n- Email will be from `git config user.email`\n- Falls back to organization-based values if SSH/git config not found\n\n#### GitHub Actions Mode\n\nIn GitHub Actions (the default), credentials always use the organization-based fallback\npattern unless explicitly provided via action inputs:\n\n```yaml\n- uses: lfreleng-actions/github2gerrit-action@main\n  with:\n    ORGANIZATION: ${{ github.repository_owner }}  # e.g., \"onap\"\n    # Credentials auto-derived:\n    # - GERRIT_SSH_USER_G2G: onap.gh2gerrit\n    # - GERRIT_SSH_USER_G2G_EMAIL: releng+onap-gh2gerrit@linuxfoundation.org\n```\n\nTo override with custom credentials:\n\n```yaml\n- uses: lfreleng-actions/github2gerrit-action@main\n  with:\n    GERRIT_SSH_USER_G2G: ${{ vars.GERRIT_SSH_USER_G2G }}\n    GERRIT_SSH_USER_G2G_EMAIL: ${{ vars.GERRIT_SSH_USER_G2G_EMAIL }}\n    ORGANIZATION: ${{ github.repository_owner }}\n```\n\n#### Disabling Derivation\n\nTo disable automatic derivation entirely, set `G2G_ENABLE_DERIVATION=false`. This requires\nall Gerrit parameters to be explicitly provided.\n\n### Issue ID Lookup\n\nThe action supports automatic Issue ID resolution via JSON lookup when you\nomit `ISSUE_ID`. Set the `ISSUE_ID_LOOKUP_JSON` input with a valid JSON array,\nand the action will automatically look up the Issue ID based on the GitHub\nactor who created the pull request.\n\n```yaml\n- uses: lfreleng-actions/github2gerrit-action@v1\n  with:\n    GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}\n    # Automatic Issue ID lookup (pass repository variable as input)\n    ISSUE_ID_LOOKUP_JSON: ${{ vars.ISSUE_ID_LOOKUP_JSON }}\n    # ... other inputs\n```\n\n**Setup:**\n\nSet a repository or organization variable named `ISSUE_ID_LOOKUP_JSON` with a\nJSON array mapping GitHub usernames to Issue IDs:\n\n**Example JSON format:**\n\n   ```json\n   [\n     { \"key\": \"dependabot[bot]\", \"value\": \"AUTO-123\" },\n     { \"key\": \"renovate[bot]\", \"value\": \"AUTO-456\" },\n     { \"key\": \"alice\", \"value\": \"PROJ-789\" },\n     { \"key\": \"bob\", \"value\": \"PROJ-101\" }\n   ]\n   ```\n\n**Lookup Logic:**\n\n1. If you provide `ISSUE_ID` input → action uses it directly (highest priority)\n2. If `ISSUE_ID` is empty AND `ISSUE_ID_LOOKUP_JSON` is valid JSON → action automatically looks up Issue ID using `github.actor`\n3. If lookup fails or JSON is invalid → action logs a warning and skips Issue ID\n\n**Validation:**\n\n- If `ISSUE_ID_LOOKUP_JSON` contains invalid JSON, the action displays a warning: `⚠️ Warning: Issue-ID JSON was not valid`\n- Invalid JSON will not cause the workflow to fail, but the action will skip adding Issue ID\n- The warning appears in both console output and log files\n\nThis feature helps organizations automatically tag commits with\nproject-specific Issue IDs based on who creates the pull request, without\nrequiring manual configuration per PR or user.\n\n## Behavior details\n\n- Branch resolution\n  - Uses `GITHUB_BASE_REF` as the target branch for Gerrit, or defaults\n    to `master` when unset, matching the existing workflow.\n- Topic naming\n  - Uses `GH-\u003crepo\u003e-\u003cpr-number\u003e` where `\u003crepo\u003e` replaces slashes with\n    hyphens.\n- GitHub Enterprise support\n  - Direct URL mode accepts enterprise GitHub hosts when explicitly enabled\n    (default: off; use github.com by default). Enable via --allow-ghe-urls or\n    ALLOW_GHE_URLS=\"true\". The tool determines the GitHub API base URL from\n    GITHUB_API_URL or GITHUB_SERVER_URL/api/v3.\n- Change‑Id handling\n  - Single commits: the process amends each cherry‑picked commit to include a\n    `Change-Id`. The tool collects these values for querying.\n  - Squashed: collects trailers from original commits, preserves\n    `Signed-off-by`, and reuses the `Change-Id` when PRs reopen or synchronize.\n- Reviewers\n  - If empty, defaults to the Gerrit SSH user email.\n- Comments\n  - Adds a back‑reference comment in Gerrit with the GitHub PR and run\n    URL. Adds a comment on the GitHub PR with the Gerrit change URL(s).\n- Closing PRs\n  - By default, PRs are **preserved** after submission (`PRESERVE_GITHUB_PRS=true`).\n  - Set `PRESERVE_GITHUB_PRS=false` to close PRs after submission on `pull_request_target` events.\n\n## Security notes\n\n- Do not hardcode secrets or keys. Provide the private key via the\n  workflow secrets and known hosts via repository or org variables.\n- SSH handling is non-invasive: the tool creates temporary SSH files in\n  the workspace without modifying user SSH configuration or keys.\n- SSH agent scanning prevention uses `IdentitiesOnly=yes` to avoid\n  unintended key usage (e.g., signing keys requiring biometric auth).\n- Temporary SSH files are automatically cleaned up after execution.\n- All external calls should use retries and clear error reporting.\n\n## Development\n\n- Language and CLI\n  - Python 3.11+, the CLI uses Typer.\n- Packaging\n  - `pyproject.toml` with setuptools backend. Use `uv` to install and run.\n- Structure\n  - `src/github2gerrit/cli.py` (CLI entrypoint)\n  - `src/github2gerrit/core.py` (orchestration)\n  - `src/github2gerrit/gitutils.py` (subprocess and git helpers)\n- Linting and type checking\n  - Ruff and MyPy use settings in `pyproject.toml`.\n  - Run from [prek](https://github.com/j178/prek) hooks and CI.\n  - prek is a faster, Rust-based drop-in replacement for pre-commit\n    that reads the existing `.pre-commit-config.yaml` unchanged.\n- Tests\n  - Pytest with coverage targets around 80%.\n  - Add unit and integration tests for each feature.\n\n### Local setup\n\n- Install `uv` and run:\n  - `uv pip install --system .`\n  - `uv run github2gerrit --help`\n- Install prek hooks:\n  - `uv tool install prek \u0026\u0026 prek install -f`\n- Run all checks (including tests) manually:\n  - `prek run --all-files`\n- Run tests:\n  - `uv run pytest -q`\n- Lint and type check:\n  - `uv run ruff check .`\n  - `uv run ruff format .`\n  - `uv run mypy src`\n\n### Dependency management\n\n- **Update dependencies**: Use `uv lock --upgrade` to rebuild and update the `uv.lock` file with the latest compatible versions\n- **Add new dependencies**: Add to `pyproject.toml` then run `uv lock` to update the lock file\n- **Install from lock file**: `uv pip install --system .` will use the exact versions from `uv.lock`\n\n### Local testing and development\n\nTest local builds before releases with commands like:\n\n```bash\n# Test against a real PR with dry-run mode\nuv run python -m github2gerrit.cli https://github.com/onap/portal-ng-bff/pull/37 --preserve-github-prs --dry-run\n\n# Test with different options\nuv run python -m github2gerrit.cli \u003cPR_URL\u003e --help\n\n# Run the CLI directly for development\nuv run github2gerrit --help\n```\n\n### CI/CD and production usage\n\nFor CI/CD pipelines (like GitHub Actions), use `uvx` to install and run without managing virtual environments:\n\n```bash\n# Install and run in one command\nuvx github2gerrit \u003cPR_URL\u003e --dry-run\n\n# Install from a specific version or source\nuvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit \u003cPR_URL\u003e\n\n# Run with specific Python version\nuvx --python 3.11 github2gerrit \u003cPR_URL\u003e\n```\n\n**Note**: `uvx` is ideal for CI/CD as it automatically handles dependency isolation and cleanup.\n\n### Notes on parity\n\n- Inputs, outputs, and environment usage match the shell action.\n- The action assumes the same GitHub variables and secrets are present.\n- Where the shell action uses tools such as `jq` and `gh`, the Python\n  version uses library calls and subprocess as appropriate, with retries\n  and clear logging.\n\n## License\n\nApache License 2.0. See `LICENSE` for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flfreleng-actions%2Fgithub2gerrit-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flfreleng-actions%2Fgithub2gerrit-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flfreleng-actions%2Fgithub2gerrit-action/lists"}