pagure_add_migration_comment: support --skip-close and --skip-comment
Also do fewer API calls.
This is a follow-up to 4303ca8b0 and !2 that enhances it.
Assisted-by: Cursor AI
This commit is contained in:
parent
4303ca8b07
commit
4b723e50ee
5 changed files with 417 additions and 138 deletions
|
|
@ -1,4 +1,13 @@
|
|||
<!-- 504d7c33-2436-4205-884c-2cb2ec52fbaa c2f0ff49-88d0-4799-a32e-d784da88f306 -->
|
||||
---
|
||||
name: Forge Migrate Story Points Script
|
||||
overview: ""
|
||||
todos:
|
||||
- id: 64ccca89-b4a6-4dcd-b417-b927fc5e2077
|
||||
content: Create forge_migrate_story_points.py with URL parsing, story points normalization, and Forge label API integration
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Forge Migrate Story Points Script
|
||||
|
||||
## Overview
|
||||
|
|
@ -87,8 +96,4 @@ python forge_migrate_story_points.py https://pagure.io/fedora-qa/repo \
|
|||
python forge_migrate_story_points.py https://pagure.io/fedora-qa/repo \
|
||||
https://forge.fedoraproject.org/quality/repo \
|
||||
--token YOUR_TOKEN --issues 1 2 3
|
||||
```
|
||||
|
||||
### To-dos
|
||||
|
||||
- [ ] Create forge_migrate_story_points.py with URL parsing, story points normalization, and Forge label API integration
|
||||
```
|
||||
|
|
@ -1,4 +1,13 @@
|
|||
<!-- 0aaf11c0-c26e-4416-b495-4d751068a277 562bc839-4e10-4657-ad9c-54c111860188 -->
|
||||
---
|
||||
name: Pagure Create Issues Script
|
||||
overview: ""
|
||||
todos:
|
||||
- id: 49651eb4-a421-4a21-9111-bac8040bcf4a
|
||||
content: Create pagure_create_issues.py with argument parsing and issue creation logic
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Pagure Create Issues Script
|
||||
|
||||
## Overview
|
||||
|
|
@ -35,8 +44,4 @@ Where N ranges from 1 to the specified count.
|
|||
|
||||
### Output
|
||||
|
||||
Similar to existing script - show progress for each issue, summary at end with success/failure counts.
|
||||
|
||||
### To-dos
|
||||
|
||||
- [ ] Create pagure_create_issues.py with argument parsing and issue creation logic
|
||||
Similar to existing script - show progress for each issue, summary at end with success/failure counts.
|
||||
237
.cursor/plans/refactor-migration-script.plan.md
Normal file
237
.cursor/plans/refactor-migration-script.plan.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
---
|
||||
name: Refactor migration script
|
||||
overview: Refactor `pagure_add_migration_comment.py` to make comment/close composable (close by default, opt out with --skip-close), eliminate redundant API calls, fix the "already migrated" skip path, and restore the simpler `(processed, skipped, failed)` summary counters.
|
||||
todos:
|
||||
- id: refactor-api-helpers
|
||||
content: Replace get_issue_comments/issue_is_opened/add_comment/close_issue with get_issue (for --issues only) + update_issue; use full dicts from get_pagure_issues in bulk mode
|
||||
status: completed
|
||||
- id: rewrite-process-issues
|
||||
content: "Rewrite process_issues loop: new skip/close logic, (processed, skipped, failed) counters, pass new flags"
|
||||
status: completed
|
||||
- id: add-cli-args
|
||||
content: Add --issue-status, --skip-close, --skip-comment with validation and Pagure status mapping
|
||||
status: completed
|
||||
- id: update-readme
|
||||
content: Document new flags and Pagure token ACL for closing
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Refactor pagure migration comment + close behavior
|
||||
|
||||
## Assessment: your proposals make sense
|
||||
|
||||
All five changes address real problems in commit `fd3e7d6`:
|
||||
|
||||
| Proposal | Verdict |
|
||||
|----------|---------|
|
||||
| Merge into `update_issue` | Good — same auth/URL pattern, clearer orchestration |
|
||||
| Drop `issue_is_opened()` | Good — but only if status comes from data you already fetch |
|
||||
| Restore `(processed, skipped, failed)` | Good — aligns with [`forge_migrate_story_points.py`](forge_migrate_story_points.py) |
|
||||
| Don't skip when close is still needed | Important bug fix when auto-close is enabled (default) |
|
||||
| New CLI flags | Good — makes comment/close/status filter explicit; close stays on by default |
|
||||
|
||||
The current commit closes **unconditionally** after every successful comment and adds **2 extra GETs per issue** (`issue_is_opened` after comment, and again in dry-run). Your refactor fixes both.
|
||||
|
||||
---
|
||||
|
||||
## Data fetching strategy (bulk vs `--issues`)
|
||||
|
||||
The Pagure issues list API (`GET .../issues`) returns **full issue objects** per page — each entry already includes `status` and `comments` (confirmed on [fedora-qa/oraculum/issues](https://pagure.io/api/0/fedora-qa/oraculum/issues)). No per-issue GET is needed in bulk mode.
|
||||
|
||||
| Mode | How issues are loaded | GETs needed |
|
||||
|------|----------------------|-------------|
|
||||
| Default (all issues) | Paginated `get_pagure_issues` — keep full dicts | 1 GET per page only |
|
||||
| `--issues N M ...` | `get_issue` per number — keep full dicts | 1 GET per listed issue |
|
||||
|
||||
In both cases, `process_issues` iterates over stored issue dicts and reads `issue["status"]` and `issue.get("comments", [])` directly. Remove `get_issue_comments()` entirely.
|
||||
|
||||
After mutations (POST comment / POST close), in-memory dicts are stale, but that does not matter — each issue is processed once and we move on.
|
||||
|
||||
---
|
||||
|
||||
## Architecture after refactor
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start[For each issue dict] --> filter{Matches --issue-status?}
|
||||
filter -->|no| skipStatus[Skip silently or log]
|
||||
filter -->|yes| needComment{Add comment?}
|
||||
needComment -->|yes, missing| doComment[POST comment]
|
||||
needComment -->|yes, exists| skipComment[Skip comment]
|
||||
needComment -->|no --skip-comment| skipComment
|
||||
doComment --> commentOK{OK?}
|
||||
commentOK -->|no| failed[failed++]
|
||||
skipComment --> needClose{"not --skip-close and Open?"}
|
||||
commentOK -->|yes| needClose
|
||||
needClose -->|yes| doClose[POST status Closed]
|
||||
needClose -->|no, nothing done| skipped[skipped++]
|
||||
needClose -->|no, work done| processed[processed++]
|
||||
doClose --> closeOK{OK?}
|
||||
closeOK -->|yes| processed
|
||||
closeOK -->|no| failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Replace `add_comment_to_issue` + `close_issue` with `update_issue`
|
||||
|
||||
New function in [`pagure_add_migration_comment.py`](pagure_add_migration_comment.py):
|
||||
|
||||
```python
|
||||
def update_issue(
|
||||
api_base, project, issue_id, api_token,
|
||||
*, comment: str | None = None, close: bool = False, issue_status: str | None = None,
|
||||
) -> bool:
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- If `comment` is not `None` → POST to `.../comment`
|
||||
- If `close` is `True` and `issue_status == "Open"` → POST to `.../status` with `status=Closed`
|
||||
- If `close` is `True` and already `"Closed"` → no-op (success)
|
||||
- Return `True` only if **all requested** operations succeed; on first failure, stop and return `False`
|
||||
|
||||
Keep error printing inside `update_issue` (same style as today).
|
||||
|
||||
---
|
||||
|
||||
## 2. Drop `issue_is_opened()` and `get_issue_comments()` — use stored issue dicts
|
||||
|
||||
**Remove** both functions. Issue data comes from:
|
||||
|
||||
- **Bulk mode:** `get_pagure_issues()` already returns `list[dict]` with full issue JSON — pass these through unchanged (stop stubbing or re-fetching).
|
||||
- **`--issues` mode:** add a `get_issue()` helper (same pattern as [`forge_migrate_story_points.py`](forge_migrate_story_points.py)) that returns the full issue dict from `GET .../issue/{id}`. Fetch all requested issues upfront into a list before the processing loop.
|
||||
|
||||
Inside the loop, always use the stored dict:
|
||||
|
||||
```python
|
||||
issue_id = issue["id"]
|
||||
status = issue["status"]
|
||||
comments = issue.get("comments", [])
|
||||
```
|
||||
|
||||
No per-issue GET inside the loop in either mode.
|
||||
|
||||
---
|
||||
|
||||
## 3. Restore `process_issues` return type and counter semantics
|
||||
|
||||
Return `tuple[int, int, int]` again: `(processed, skipped, failed)`.
|
||||
|
||||
Per issue, after determining what actions are needed:
|
||||
|
||||
| Situation | Counter |
|
||||
|-----------|---------|
|
||||
| Nothing to do (comment exists or skipped, and not closing / already closed) | `skipped` |
|
||||
| All requested actions succeed (comment and/or close) | `processed` |
|
||||
| Any requested action fails | `failed` |
|
||||
| Fetch error | `failed` |
|
||||
|
||||
**Dry run:** increment `processed` when work *would* be done; `skipped` when nothing would happen.
|
||||
|
||||
Remove `closed` / `failed_to_close` from summary output in `main()`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Fix the `has_migration_comment` branch
|
||||
|
||||
Current code (lines 321–327) always `continue`s. New logic:
|
||||
|
||||
```python
|
||||
needs_comment = not skip_comment and not has_migration_comment(comments)
|
||||
needs_close = not skip_close and status == "Open"
|
||||
|
||||
if not needs_comment and not needs_close:
|
||||
print(" Skipping: nothing to do")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if has_migration_comment(comments) and not skip_comment:
|
||||
print(" Migration comment already exists")
|
||||
# then proceed to update_issue(...)
|
||||
```
|
||||
|
||||
This enables `--skip-comment` reruns on already-commented open tickets (close still runs by default).
|
||||
|
||||
---
|
||||
|
||||
## 5. New CLI arguments
|
||||
|
||||
Add to `argparse` in [`pagure_add_migration_comment.py`](pagure_add_migration_comment.py):
|
||||
|
||||
| Flag | Values | Default | Effect |
|
||||
|------|--------|---------|--------|
|
||||
| `--issue-status` | `open`, `closed`, `all` | `all` | Filter which issues to process |
|
||||
| `--skip-close` | flag | off | Do **not** close open issues (by default, open issues **are** closed) |
|
||||
| `--skip-comment` | flag | off | Do not post migration comment |
|
||||
|
||||
**Default behavior:** add migration comment **and** close open issues (same intent as commit `fd3e7d6`, but now explicit and controllable).
|
||||
|
||||
**Startup validation:** if `--skip-comment` and `--skip-close`, exit with error — otherwise the script would do nothing useful.
|
||||
|
||||
**Pagure API mapping:** user-facing lowercase → Pagure values passed to `get_pagure_issues`:
|
||||
- `open` → `"Open"`
|
||||
- `closed` → `"Closed"`
|
||||
- `all` → `"all"`
|
||||
|
||||
Wire `--issue-status` into the existing `get_pagure_issues(..., status=...)` call (currently hardcoded/default only).
|
||||
|
||||
Pass `skip_comment`, `skip_close`, and mapped status through to `process_issues`.
|
||||
|
||||
---
|
||||
|
||||
## Things to consider (not blockers, but worth deciding)
|
||||
|
||||
### `--issues` + `--issue-status` interaction
|
||||
|
||||
Fetch full issue dicts upfront via `get_issue()`, then apply `--issue-status` filter client-side on `issue["status"]` before entering the mutation logic. Issues not matching the filter are skipped (log optionally, do not count as failed).
|
||||
|
||||
Note: when `--issue-status open` is used **without** `--issues`, the Pagure API filter handles it server-side via `get_pagure_issues(status="Open")` — no client-side filter needed in bulk mode.
|
||||
|
||||
### Pagure token ACLs
|
||||
|
||||
README currently lists only "Comment on a ticket". Closing requires **`issue_change_status`** or **`issue_update`** on the Pagure API token. Update [README.md](README.md) to note that closing is the default and requires the extra ACL; mention `--skip-close` to disable.
|
||||
|
||||
### Partial failure policy
|
||||
|
||||
If comment succeeds but close fails → count as `failed`, not `processed`. Do **not** attempt close after a comment failure (keeps behavior predictable).
|
||||
|
||||
### Idempotent close
|
||||
|
||||
Closing an already-closed issue should be a no-op success, not a failure or skip — important for reruns.
|
||||
|
||||
### `--issue-status closed` with default close behavior
|
||||
|
||||
Harmless but pointless (closed issues won't be closed again). No special case needed; they'll land in `skipped`.
|
||||
|
||||
### Rate limiting / `--delay`
|
||||
|
||||
Bulk mode drops from **1 GET per issue + POSTs** down to **pagination GETs + POSTs only** — a major improvement for large repos. Keep `--delay` between pagination pages (already in `get_pagure_issues`) and after each issue's mutation phase. For `--issues` mode, apply delay between individual `get_issue` fetches (same as `forge_migrate_story_points.py`).
|
||||
|
||||
---
|
||||
|
||||
## Files to change
|
||||
|
||||
- [`pagure_add_migration_comment.py`](pagure_add_migration_comment.py) — main refactor (API helpers, `process_issues`, CLI, summary)
|
||||
- [`README.md`](README.md) — document new flags and closing ACL requirement (minimal update)
|
||||
|
||||
No tests exist in this repo; manual verification with `--dry-run` on staging is the test plan.
|
||||
|
||||
## Suggested manual test matrix
|
||||
|
||||
```bash
|
||||
# Default: comment + close open issues, all statuses
|
||||
--dry-run
|
||||
|
||||
# Comment only, no closing (e.g. token lacks close ACL)
|
||||
--skip-close --dry-run
|
||||
|
||||
# Close-only rerun on already-commented open issues
|
||||
--skip-comment --issue-status open --dry-run
|
||||
|
||||
# Comment + close, open issues only
|
||||
--issue-status open --dry-run
|
||||
|
||||
# Invalid: should error at startup
|
||||
--skip-comment --skip-close
|
||||
```
|
||||
19
README.md
19
README.md
|
|
@ -20,7 +20,10 @@ Several scripts require a Pagure API token for authentication. To create one:
|
|||
1. Log in to [Pagure](https://pagure.io/)
|
||||
2. Go to your [API settings](https://pagure.io/settings#nav-api-tab)
|
||||
3. Click "Create new API key"
|
||||
4. Select the required ACLs (e.g., "Comment on a ticket" for adding migration comments)
|
||||
4. Select the required ACLs:
|
||||
- **Comment on a ticket (issue_comment)** — required for posting migration comments
|
||||
- **Update an issue, status, comments, custom fields... (issue_update)** — required for closing issues
|
||||
- **Create a new ticket (issue_create)** — required for `pagure_create_issues.py`
|
||||
5. Save the token to a file (e.g., `~/.pagure-token`) for use with `--token-file`
|
||||
|
||||
## Forge (Forgejo) API Token
|
||||
|
|
@ -37,7 +40,7 @@ Several scripts require a Forge API token for authentication. To create one:
|
|||
|
||||
### pagure_add_migration_comment.py
|
||||
|
||||
Adds a migration notice comment to Pagure tickets, linking to their new Forge location.
|
||||
Adds a migration notice comment to Pagure tickets, linking to their new Forge location. By default, open issues are also closed after the comment is posted.
|
||||
|
||||
**Usage:**
|
||||
|
||||
|
|
@ -51,8 +54,20 @@ python pagure_add_migration_comment.py https://pagure.io/fedora-qa/some-repo \
|
|||
python pagure_add_migration_comment.py https://stg.pagure.io/fedora-qa/some-repo \
|
||||
https://forge.stg.fedoraproject.org/quality/some-repo \
|
||||
--token-file ~/.pagure-token --issues 1 2 3
|
||||
|
||||
# Comment only, without closing
|
||||
python pagure_add_migration_comment.py https://pagure.io/fedora-qa/some-repo \
|
||||
https://forge.fedoraproject.org/quality/some-repo \
|
||||
--token-file ~/.pagure-token --skip-close
|
||||
|
||||
# Close open issues that already have a migration comment
|
||||
python pagure_add_migration_comment.py https://pagure.io/fedora-qa/some-repo \
|
||||
https://forge.fedoraproject.org/quality/some-repo \
|
||||
--token-file ~/.pagure-token --skip-comment --issue-status open
|
||||
```
|
||||
|
||||
Notable options: `--issue-status` (`open`, `closed`, `all`), `--skip-close`, `--skip-comment`.
|
||||
|
||||
Run with `--help` for all available options.
|
||||
|
||||
### pagure_create_issues.py
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ import time
|
|||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
DEFAULT_COMMENT_TEMPLATE = (
|
||||
"This issue has been migrated to Fedora Forge:\n{forge_url}\n\n"
|
||||
"Please continue any further discussion there."
|
||||
)
|
||||
MIGRATION_MARKER = "This issue has been migrated to Fedora Forge:"
|
||||
|
||||
PAGURE_STATUS = {"open": "Open", "closed": "Closed", "all": "all"}
|
||||
|
||||
# Ensure the marker is present in the default template to detect already-migrated issues
|
||||
assert MIGRATION_MARKER in DEFAULT_COMMENT_TEMPLATE, (
|
||||
"MIGRATION_MARKER must be contained in DEFAULT_COMMENT_TEMPLATE"
|
||||
|
|
@ -124,11 +127,11 @@ def get_pagure_issues(
|
|||
return all_issues
|
||||
|
||||
|
||||
def get_issue_comments(
|
||||
def get_issue(
|
||||
api_base: str, project: str, issue_id: int, api_token: str
|
||||
) -> list[dict]:
|
||||
) -> dict:
|
||||
"""
|
||||
Fetch comments for a specific issue.
|
||||
Fetch a single issue from Pagure.
|
||||
|
||||
Args:
|
||||
api_base: Pagure API base URL
|
||||
|
|
@ -137,16 +140,17 @@ def get_issue_comments(
|
|||
api_token: Pagure API token
|
||||
|
||||
Returns:
|
||||
List of comment dictionaries
|
||||
Issue dictionary
|
||||
|
||||
Raises:
|
||||
requests.exceptions.HTTPError: If the API call fails
|
||||
"""
|
||||
url = f"{api_base}/{project}/issue/{issue_id}"
|
||||
headers = {"Authorization": f"token {api_token}"}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return data.get("comments", [])
|
||||
return response.json()
|
||||
|
||||
|
||||
def has_migration_comment(comments: list[dict]) -> bool:
|
||||
|
|
@ -165,87 +169,48 @@ def has_migration_comment(comments: list[dict]) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def add_comment_to_issue(
|
||||
api_base: str, project: str, issue_id: int, comment: str, api_token: str
|
||||
def update_issue(
|
||||
api_base: str,
|
||||
project: str,
|
||||
issue_id: int,
|
||||
api_token: str,
|
||||
*,
|
||||
comment: str | None = None,
|
||||
close: bool = False,
|
||||
issue_status: str | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Add a comment to a Pagure issue.
|
||||
|
||||
Args:
|
||||
api_base: Pagure API base URL
|
||||
project: Pagure project path
|
||||
issue_id: Issue number
|
||||
comment: Comment text to add
|
||||
api_token: Pagure API token
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
url = f"{api_base}/{project}/issue/{issue_id}/comment"
|
||||
headers = {"Authorization": f"token {api_token}"}
|
||||
data = {"comment": comment}
|
||||
|
||||
response = requests.post(url, headers=headers, data=data, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f" Error adding comment: {response.status_code} - {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
def close_issue(
|
||||
api_base: str, project: str, issue_id: int, api_token: str
|
||||
) -> bool:
|
||||
"""
|
||||
Close a Pagure issue.
|
||||
Update a Pagure issue by adding a comment and/or closing it.
|
||||
|
||||
Args:
|
||||
api_base: Pagure API base URL
|
||||
project: Pagure project path
|
||||
issue_id: Issue number
|
||||
api_token: Pagure API token
|
||||
comment: Comment text to add, or None to skip
|
||||
close: If True, close the issue when it is Open
|
||||
issue_status: Current issue status ('Open' or 'Closed')
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
True if all requested operations succeed, False otherwise
|
||||
"""
|
||||
url = f"{api_base}/{project}/issue/{issue_id}/status"
|
||||
headers = {"Authorization": f"token {api_token}"}
|
||||
data = {"status": "Closed"}
|
||||
|
||||
response = requests.post(url, headers=headers, data=data, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f" Error closing issue: {response.status_code} - {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
def issue_is_opened(
|
||||
api_base: str, project: str, issue_id: int, api_token: str
|
||||
) -> bool:
|
||||
"""
|
||||
Check if issue is opened.
|
||||
|
||||
Args:
|
||||
api_base: Pagure API base URL
|
||||
project: Pagure project path
|
||||
issue_id: Issue number
|
||||
api_token: Pagure API token
|
||||
|
||||
Returns:
|
||||
True if issue is opened, False otherwise
|
||||
"""
|
||||
url = f"{api_base}/{project}/issue/{issue_id}"
|
||||
headers = {"Authorization": f"token {api_token}"}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if comment is not None:
|
||||
url = f"{api_base}/{project}/issue/{issue_id}/comment"
|
||||
response = requests.post(url, headers=headers, data={"comment": comment}, timeout=30)
|
||||
if response.status_code != 200:
|
||||
print(f" Error adding comment: {response.status_code} - {response.text}")
|
||||
return False
|
||||
|
||||
status = data.get("status", [])
|
||||
return status and status == "Open"
|
||||
if close and issue_status == "Open":
|
||||
url = f"{api_base}/{project}/issue/{issue_id}/status"
|
||||
response = requests.post(url, headers=headers, data={"status": "Closed"}, timeout=30)
|
||||
if response.status_code != 200:
|
||||
print(f" Error closing issue: {response.status_code} - {response.text}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def construct_forge_issue_url(forge_url: str, issue_id: int) -> str:
|
||||
|
|
@ -270,9 +235,12 @@ def process_issues(
|
|||
forge_url: str,
|
||||
api_token: str,
|
||||
issue_numbers: list[int] | None,
|
||||
issue_status: str,
|
||||
skip_comment: bool,
|
||||
skip_close: bool,
|
||||
dry_run: bool,
|
||||
delay: float,
|
||||
) -> tuple[int, int, int, int, int]:
|
||||
) -> tuple[int, int, int]:
|
||||
"""
|
||||
Process issues and add migration comments.
|
||||
|
||||
|
|
@ -282,83 +250,107 @@ def process_issues(
|
|||
forge_url: Full URL to the Forge project
|
||||
api_token: Pagure API token
|
||||
issue_numbers: Specific issue numbers to process, or None for all
|
||||
dry_run: If True, don't actually add comments
|
||||
issue_status: Pagure status filter ('all', 'Open', 'Closed')
|
||||
skip_comment: If True, do not post migration comments
|
||||
skip_close: If True, do not close open issues
|
||||
dry_run: If True, don't actually modify issues
|
||||
delay: Delay between API calls in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (processed, skipped, failed) counts
|
||||
"""
|
||||
processed = 0
|
||||
closed = 0
|
||||
failed_to_close = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
if issue_numbers:
|
||||
# Process specific issues
|
||||
issues = [{"id": num} for num in issue_numbers]
|
||||
print(f"Fetching {len(issue_numbers)} specific issue(s) from {project}...")
|
||||
issues = []
|
||||
for issue_id in issue_numbers:
|
||||
try:
|
||||
issue = get_issue(api_base, project, issue_id, api_token)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f" Error fetching issue #{issue_id}: {e}")
|
||||
failed += 1
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
continue
|
||||
|
||||
if issue_status != "all" and issue["status"] != issue_status:
|
||||
print(
|
||||
f" Skipping #{issue_id}: status is {issue['status']} "
|
||||
f"(filter: {issue_status})"
|
||||
)
|
||||
skipped += 1
|
||||
else:
|
||||
issues.append(issue)
|
||||
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
else:
|
||||
# Fetch all issues
|
||||
print(f"Fetching issues from {project}...")
|
||||
issues = get_pagure_issues(api_base, project, api_token, delay=delay)
|
||||
issues = get_pagure_issues(
|
||||
api_base, project, api_token, status=issue_status, delay=delay
|
||||
)
|
||||
print(f"Found {len(issues)} issues")
|
||||
|
||||
for issue in issues:
|
||||
issue_id = issue["id"]
|
||||
status = issue["status"]
|
||||
comments = issue.get("comments", [])
|
||||
forge_issue_url = construct_forge_issue_url(forge_url, issue_id)
|
||||
comment = DEFAULT_COMMENT_TEMPLATE.format(forge_url=forge_issue_url, issue_id=issue_id)
|
||||
comment_text = DEFAULT_COMMENT_TEMPLATE.format(
|
||||
forge_url=forge_issue_url, issue_id=issue_id
|
||||
)
|
||||
|
||||
print(f"\nProcessing issue #{issue_id}...")
|
||||
|
||||
# Check for existing migration comment
|
||||
try:
|
||||
comments = get_issue_comments(api_base, project, issue_id, api_token)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f" Error fetching issue: {e}")
|
||||
failed += 1
|
||||
continue
|
||||
requested_comment = not skip_comment
|
||||
requested_close = not skip_close
|
||||
needs_comment = requested_comment and not has_migration_comment(comments)
|
||||
needs_close = not skip_close and status == "Open"
|
||||
|
||||
if has_migration_comment(comments):
|
||||
print(" Skipping: migration comment already exists")
|
||||
if requested_comment and has_migration_comment(comments):
|
||||
print(" Migration comment already exists")
|
||||
|
||||
if requested_close and status == "Closed":
|
||||
print(" Issue is already closed")
|
||||
|
||||
if not needs_comment and not needs_close:
|
||||
print(" Skipping: nothing to do")
|
||||
skipped += 1
|
||||
# Delay after API call to avoid rate limiting
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
print(f" [DRY RUN] Would add comment linking to: {forge_issue_url}")
|
||||
if needs_comment:
|
||||
print(f" [DRY RUN] Would add comment linking to: {forge_issue_url}")
|
||||
if needs_close:
|
||||
print(" [DRY RUN] Would close issue")
|
||||
processed += 1
|
||||
# Delay after API call to avoid rate limiting
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
if issue_is_opened(api_base, project, issue_id, api_token):
|
||||
print(f" [DRY RUN] Issue is opened")
|
||||
print(f" [DRY RUN] Closing the issue")
|
||||
else:
|
||||
print(f" [DRY RUN] Issue is closed")
|
||||
|
||||
else:
|
||||
print(f" Adding migration comment linking to: {forge_issue_url}")
|
||||
if add_comment_to_issue(api_base, project, issue_id, comment, api_token):
|
||||
print(" Success!")
|
||||
if issue_is_opened(api_base, project, issue_id, api_token):
|
||||
if close_issue(api_base, project, issue_id, api_token):
|
||||
print(" Issue closed!")
|
||||
closed += 1
|
||||
else:
|
||||
failed_to_close += 1
|
||||
if needs_comment:
|
||||
print(f" Adding migration comment linking to: {forge_issue_url}")
|
||||
if needs_close:
|
||||
print(" Closing issue...")
|
||||
|
||||
if update_issue(
|
||||
api_base,
|
||||
project,
|
||||
issue_id,
|
||||
api_token,
|
||||
comment=comment_text if needs_comment else None,
|
||||
close=needs_close,
|
||||
issue_status=status,
|
||||
):
|
||||
print(" Success!")
|
||||
processed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# Delay after API call to avoid rate limiting
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
return processed, skipped, failed, closed, failed_to_close
|
||||
return processed, skipped, failed
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -372,6 +364,9 @@ Examples:
|
|||
|
||||
# Process specific issues on staging Pagure
|
||||
%(prog)s https://stg.pagure.io/fedora-qa/some-repo https://forge.fedoraproject.org/quality/some-repo --token YOUR_TOKEN --issues 1 2 3
|
||||
|
||||
# Comment only, without closing issues
|
||||
%(prog)s https://pagure.io/fedora-qa/some-repo https://forge.fedoraproject.org/quality/some-repo --token-file ~/.pagure-token --skip-close
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -400,10 +395,26 @@ Examples:
|
|||
nargs="+",
|
||||
help="Specific issue numbers to process (default: all issues)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--issue-status",
|
||||
choices=["open", "closed", "all"],
|
||||
default="all",
|
||||
help="Only process issues with this status (default: all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-close",
|
||||
action="store_true",
|
||||
help="Do not close open issues (by default, open issues are closed)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-comment",
|
||||
action="store_true",
|
||||
help="Do not post migration comment (by default, migration comment is posted)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview changes without actually adding comments",
|
||||
help="Preview changes without actually modifying issues",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
|
|
@ -414,6 +425,10 @@ Examples:
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.skip_comment and args.skip_close:
|
||||
print("Error: --skip-comment and --skip-close together would do nothing")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse the Pagure URL
|
||||
try:
|
||||
api_base, project = parse_pagure_url(args.pagure_url)
|
||||
|
|
@ -434,6 +449,8 @@ Examples:
|
|||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
pagure_status = PAGURE_STATUS[args.issue_status]
|
||||
|
||||
print(f"Pagure URL: {args.pagure_url}")
|
||||
print(f"Forge URL: {args.forge_url}")
|
||||
if args.dry_run:
|
||||
|
|
@ -441,12 +458,15 @@ Examples:
|
|||
print()
|
||||
|
||||
try:
|
||||
processed, skipped, failed, closed, failed_to_close = process_issues(
|
||||
processed, skipped, failed = process_issues(
|
||||
api_base=api_base,
|
||||
project=project,
|
||||
forge_url=args.forge_url,
|
||||
api_token=api_token,
|
||||
issue_numbers=args.issues,
|
||||
issue_status=pagure_status,
|
||||
skip_comment=args.skip_comment,
|
||||
skip_close=args.skip_close,
|
||||
dry_run=args.dry_run,
|
||||
delay=args.delay,
|
||||
)
|
||||
|
|
@ -460,10 +480,8 @@ Examples:
|
|||
print(f"\n{'=' * 40}")
|
||||
print("Summary:")
|
||||
print(f" Processed: {processed}")
|
||||
print(f" Closed: {closed}")
|
||||
print(f" Skipped (already migrated): {skipped}")
|
||||
print(f" Skipped: {skipped}")
|
||||
print(f" Failed: {failed}")
|
||||
print(f" Failed to close: {failed_to_close}")
|
||||
|
||||
if failed > 0:
|
||||
sys.exit(1)
|
||||
|
|
@ -471,4 +489,3 @@ Examples:
|
|||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue