6
mirror of https://github.com/AllSpiceIO/post-dr-comment.git synced 2025-04-24 16:23:21 +00:00
post-dr-comment/post_dr_comment.py
Shrikanth Upadhayaya 831bd5cb0f
Raise a clearer error if design review number is not set
If `post-dr-comment` is called in a run which is not on a design review,
the DR number is not set in the context. This previously led to an
incomprehensible error. This commit checks early to make sure the design
review number is set and errors with an explanation if it isn't.
2025-03-17 15:21:18 -04:00

209 lines
5.9 KiB
Python
Executable File

#! /usr/bin/env -S python3
"""
post_dr_comment.py: Post a comment to an AllSpice Hub Design Review.
"""
import argparse
import logging
import os
import sys
import yaml
from typing import Tuple
from allspice import AllSpice, Comment, DesignReview
COMMENT_IDENTIFIER = "<!-- AllSpice Hub Auto-DR Comment -->"
logger = logging.getLogger(__name__)
def parse_bool(input: str | bool) -> bool:
"""
Parse a YAML-like boolean string as a boolean.
"""
if isinstance(input, bool):
return input
if input.lower() in ("yes", "true", "t", "y", "1"):
return True
elif input.lower() in ("no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError(
"One of: yes, no, true, false, t, f, y, n, 1, 0 expected."
)
def parse_front_matter(comment_body: str) -> Tuple[dict, str]:
"""
Check if the comment body has a front matter and parse it.
Returns the front matter as a dictionary and the comment body without the
front matter.
If the front matter is empty or missing, the front matter dictionary will
be empty.
"""
front_matter = {}
found_front_matter = False
stripped_comment_body = comment_body.strip()
if stripped_comment_body.startswith("---"):
split_comment = stripped_comment_body.split("---", 2)
if len(split_comment) == 3:
found_front_matter = True
try:
front_matter = yaml.safe_load(split_comment[1])
comment_body = split_comment[2].lstrip()
found_front_matter = True
except yaml.YAMLError as e:
logger.error(f"Failed to parse front matter: {e}")
if not found_front_matter:
logger.info("No front matter found in comment body.")
return front_matter, comment_body
def upsert_comment(design_review: DesignReview, comment_body: str) -> Comment:
"""
Upsert a comment on a Design Review.
If a comment with the same identifier already exists, update the existing
comment. Otherwise, create a new comment.
Returns the created or updated comment.
"""
existing_comment = None
comments = design_review.get_comments()
updated_comment_body = f"{COMMENT_IDENTIFIER}\n{comment_body}"
for comment in comments:
if COMMENT_IDENTIFIER in comment.body:
existing_comment = comment
break
if existing_comment:
logger.info("Updating existing comment.")
existing_comment.body = updated_comment_body
existing_comment.commit()
return existing_comment
else:
logger.info("Creating new comment.")
return design_review.create_comment(updated_comment_body)
def upsert_attachments(comment: Comment, attachments: list[str]):
"""
Upsert attachments to a comment.
This clears all existing attachments and then adds new attachments.
"""
existing_attachments = comment.get_attachments()
for attachment in existing_attachments:
comment.delete_attachment(attachment)
for attachment in attachments:
with open(attachment, "rb") as f:
try:
comment.create_attachment(f)
except Exception as e:
if "500" in str(e):
logger.error(
f"Failed to upload attachment {attachment}. "
"The file may be too large, or it may be of a file type "
"that is not supported by the AllSpice Hub."
)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description=__doc__,
)
parser.add_argument(
"--allspice-hub-url",
required=False,
default="https://hub.allspice.io",
help="The URL of the AllSpice Hub DR to post the comment to.",
)
parser.add_argument(
"--repository",
required=True,
help="The repository that the design review is associated with.",
)
parser.add_argument(
"--design-review-number",
required=True,
help="The number of the design review to post the comment to.",
)
parser.add_argument(
"--comment-path",
required=True,
help="The path to the Comment Markdown file.",
)
parser.add_argument(
"--reuse-existing-comment",
required=False,
default=True,
type=parse_bool,
help="Whether to reuse an existing comment if it exists.",
)
parser.add_argument(
"--log-level",
required=False,
default="INFO",
help="The logging level to use.",
)
token = os.getenv("ALLSPICE_AUTH_TOKEN")
if not token:
raise ValueError("ALLSPICE_AUTH_TOKEN environment variable not set.")
args = parser.parse_args()
if not args.design_review_number.isdigit():
raise ValueError(
"Design review number is either not set or not a number; this run may not be on a pull request event?"
)
logger.setLevel(args.log_level.upper())
client = AllSpice(
args.allspice_hub_url,
token_text=token,
log_level=args.log_level.upper(),
)
owner, repo = args.repository.split("/")
design_review = DesignReview.request(client, owner, repo, args.design_review_number)
with open(args.comment_path, "r") as f:
comment_body = f.read()
front_matter, comment_body = parse_front_matter(comment_body)
attachments = []
if front_matter:
logger.debug(f"Front matter: {front_matter}")
if "attachments" in front_matter:
attachments = front_matter["attachments"]
if args.reuse_existing_comment:
comment = upsert_comment(design_review, comment_body)
else:
comment = design_review.create_comment(comment_body)
if attachments:
upsert_attachments(comment, attachments)
logger.info("Comment posted successfully.")
if __name__ == "__main__":
main()