Skip to content
Merged
96 changes: 96 additions & 0 deletions gh_review_project/cr_deadline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import argparse
from pathlib import Path
from review_project import ProjectData, ISSUE_ID


def remove_milestone(
issue_data: ProjectData, milestone: str, dry_run: bool = False
) -> int:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"""
Remove the milestone from all open issues that do not have any linked PRs
attached to them. Leave a comment explaining why.
"""

open_issues = issue_data.get_milestone(milestone=milestone, status="open")

comment = (
f"[Automatic Update]\n\nThe Code Review deadline for the {milestone} has "
f"passed. As this issue does not have a linked pull request it has been "
f"removed from the milestone. Please review this issue and either select "
f"a new milestone or close it as appropriate. Please contact "
f"@MetOffice/ssdteam if you think there has been an error.\n\n Thanks"
)

for repo in open_issues:
print(f"\nRemoving issues in {repo}")
for issue in open_issues[repo]:
if not issue.linked_prs:
issue.add_comment(comment, dry_run=dry_run)
issue.modify_milestone(milestone=None, dry_run=dry_run)


def parse_args():
"""
Read command line args
"""

testfile_path = Path(__file__).parent / "test"

parser = argparse.ArgumentParser(
"Changes to the Simulation System projects required at the code"
"review deadline."
)

parser.add_argument("--milestone", help="Milestone being released")
parser.add_argument(
"--test",
action="store_true",
help="Use test input files.",
)
parser.add_argument(
"--capture_project",
action="store_true",
help="Capture the current project status into the test file",
)
parser.add_argument(
"--file",
default=testfile_path,
help="Filepath to test data for either capturing the project status, "
"or use as input data.",
)
parser.add_argument(
"--dry",
action="store_true",
help="Dry run. Print commands, don't action them. Always true when "
"running with test data.",
)

args = parser.parse_args()

args.file = Path(args.file)
args.file = args.file.expanduser().resolve()

if args.test:
args.dry = True

return args


def main(
milestone: str, test: bool, capture_project: bool, file: Path, dry: bool
) -> None:

# Get milestone data
if test:
issue_data = ProjectData.from_file(ISSUE_ID, file / "issue.json")
else:
issue_data = ProjectData.from_github(
ISSUE_ID, capture_project, file / "issue.json"
)

remove_milestone(issue_data, milestone=milestone, dry_run=dry)


if __name__ == "__main__":
args = parse_args()
main(args.milestone, args.test, args.capture_project, args.file, args.dry)
155 changes: 87 additions & 68 deletions gh_review_project/finish_milestone.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""
from pathlib import Path
import argparse
from review_project import ProjectData
from review_project import ProjectData, REVIEW_ID, ISSUE_ID


def print_banner(message: str) -> None:
Expand All @@ -25,85 +25,70 @@ def print_banner(message: str) -> None:
print("=" * len(message))


def still_open(open_prs: dict, current_milestone: str) -> int:
"""
Report on all open pull requests for the current milestone
"""

print_banner(f"Checking for open pull requests for {current_milestone}")

total = 0

for repo in open_prs:
print(f"{repo} \n{'-'*len(repo)}")
for pr in open_prs[repo]:
print(f"{pr.status: <18} #{pr.number: <5} {pr.title}")

count = len(open_prs[repo])
print(f"-> {count} open pull request(s) in {repo} \n")
total += count

if total == 0:
print(f"No open pull requests for {current_milestone} \n")

return total


def closed_other(
closed_prs: dict, current_milestone: str, dry_run: bool = False
) -> int:
"""
Report on closed pull requests not at the current milestone.
reviews: ProjectData, current_milestone: str, dry_run: bool = False
) -> None:
"""
Set a milestone for closed PRs without one.

print_banner(f"Checking for closed pull requests not for {current_milestone}")

total = 0

for milestone in closed_prs:
if milestone == current_milestone:
continue

elif milestone == "None":
print(f"Setting pull requests with no milestone " f"to {current_milestone}")
for repo in closed_prs[milestone]:
for pr in closed_prs[milestone][repo]:
pr.modify_milestone(current_milestone, dry_run)
reviews: ProjectData from the Review Tracker Project
current_milestone: Milestone being closed
dry_run: If true, do not actually modify the milestone
"""

else:
for repo in closed_prs[milestone]:
print(f"{repo} \n{'-' * len(repo)}")
for pr in closed_prs[milestone][repo]:
print(f"#{pr.number : <5} {pr.title}")
print_banner(f"Setting pull requests with no milestone to {current_milestone}")

count = len(closed_prs[milestone][repo])
print(
f"-> {count} closed pull request(s) in {repo} at milestone {milestone} \n"
)
total += count
closed_prs = reviews.get_milestone(milestone="None", status="closed")

return total
for repo in closed_prs:
for pr in closed_prs[repo]:
pr.modify_milestone(current_milestone, dry_run)


def check_ready(data: ProjectData, milestone: str, dry_run: bool = False) -> None:
def check_ready(
reviews: ProjectData, issues: ProjectData, current_milestone: str
) -> None:
"""
Check if the milestone is ready to be closed by confirming that:
* all pull requests for this milestone have been completed
* all closed pull requests in the project are in this milestone.
* all In Review issues for this milestone have been completed

Give the user the choice to continue regardless since there may be valid
exceptions.
"""
open_prs = data.get_milestone(milestone=milestone, status="open")
closed_prs = data.get_all_milestones(status="closed")
total_open = still_open(open_prs, milestone)
total_other = closed_other(closed_prs, milestone, dry_run)
print_banner(f"Checking for open pull requests for {current_milestone}")
total_open = reviews.count_items(
milestone=current_milestone, status="open", message="open pull requests"
)
if total_open == 0:
print("No open pull requests\n")

print_banner(f"Checking for issues in review for {current_milestone}")
total_issues_in_review = issues.count_items(
milestone=current_milestone, status="In Review", message="In Review issues"
)
if total_issues_in_review == 0:
print("No issues in review\n")

if total_open or total_other:
print_banner(f"Checking for closed pull requests not set to {current_milestone}")
total_other = 0
for milestone in reviews.milestones:
if milestone == current_milestone:
continue
else:
total_other += reviews.count_items(
milestone=milestone, status="closed", message="closed pull requests"
)
if total_other == 0:
print("All closed pull requests are in this milestone\n")

if total_open or total_other or total_issues_in_review:
print("=" * 50)
print(
f"{total_open} open pull request(s) in {milestone} and "
f"{total_other} closed pull request(s) not in {milestone}."
f"{total_open} open pull request(s) in {current_milestone}. \n"
f"{total_other} closed pull request(s) not in {current_milestone}. \n"
f"{total_issues_in_review} issues in {current_milestone} with status In Review. "
)
cont = input("Would you like to continue with closing this milestone? (y/n) ")

Expand Down Expand Up @@ -131,12 +116,32 @@ def report(data: ProjectData, milestone: str) -> None:
print(f"{total} pull requests completed in {milestone}")


def tidy_issues(issue_data: ProjectData, milestone: str, dry_run: bool = False) -> None:
"""
Remove any outstanding open issues from the current milestone.
"""

print_banner(f"Removing uncompleted issues from {milestone}")

issues = issue_data.get_milestone(milestone=milestone, status="open")
comment = (
f"[Automatic Update]\n\nThe {milestone} milestone is being closed. "
f"Please review this issue and either select a new milestone or "
f"close it as appropriate. Please contact @MetOffice/ssdteam if "
f"you think there has been an error.\n\n Thanks"
)
for repo in issues:
for issue in issues[repo]:
issue.add_comment(comment, dry_run=dry_run)
issue.modify_milestone(milestone=None, dry_run=dry_run)


def parse_args():
"""
Read command line args
"""

testfile = Path(__file__).parent / "test" / "test.json"
testfile_path = Path(__file__).parent / "test"

parser = argparse.ArgumentParser(
"Archive milestone contents within the project and close the "
Expand All @@ -156,7 +161,7 @@ def parse_args():
)
parser.add_argument(
"--file",
default=testfile,
default=testfile_path,
help="Filepath to test data for either capturing the project status, "
"or use as input data.",
)
Expand Down Expand Up @@ -184,17 +189,31 @@ def main(

# Get milestone data
if test:
data = ProjectData.from_file(file)
review_data = ProjectData.from_file(REVIEW_ID, file / "pr.json")
issue_data = ProjectData.from_file(ISSUE_ID, file / "issue.json")
else:
data = ProjectData.from_github(capture_project, file)
review_data = ProjectData.from_github(
REVIEW_ID, capture_project, file / "pr.json"
)
issue_data = ProjectData.from_github(
ISSUE_ID, capture_project, file / "issue.json"
)

# Set a milestone on closed PRs
closed_other(review_data, milestone, dry)

# Process data and report on status
check_ready(data, milestone, dry)
report(data, milestone)
check_ready(review_data, issue_data, milestone)

# Tidy outstanding issues
tidy_issues(issue_data, milestone, dry)

# Archive pull requests at the milestone
print_banner(f"Archiving Milestone {milestone}")
data.archive_milestone(milestone, dry_run=dry)
review_data.archive_milestone(milestone, dry_run=dry)

# Print report as final step so its visible
report(review_data, milestone)

# Close milestones
# TODO: run this command from here, rather than prompting user. Leaving
Expand Down
Loading