commit 656b43d1d76a18a5e10f3effdbc59ec2ae3ff3c6 Author: Shrikanth Upadhayaya <shrik450@gmail.com> Date: Thu Sep 5 12:49:01 2024 -0700 Initialize diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f5a8323 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: monthly + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7657c07 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Lint and test + +on: + push: + branches: [main] + pull_request: + branches: ["**"] + +jobs: + test: + name: Test + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-test.txt + - name: Check formatting + run: ruff format --diff . + - name: Lint with ruff + run: ruff check --target-version=py310 . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b694934 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0048727 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12-bookworm + +COPY requirements.txt /requirements.txt +COPY post_dr_comment.py /post_dr_comment.py + +RUN pip install -r /requirements.txt + +ENTRYPOINT [ "/post_dr_comment.py" ] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8211765 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 AllSpice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad34ead --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Post Comment on a Design Review + +Post a comment on a Design Review using a markdown file as the source of the +comment on AllSpice Hub using +[AllSpice Actions](https://learn.allspice.io/docs/actions-cicd). + +## Usage + +Add the following step to your actions: + +```yaml +- name: Post Comment on Design Review + uses: https://hub.allspice.io/Actions/post-dr-comment@v0.1 + with: + # The path to the markdown file containing the comment body. + comment_path: path/to/comment.md +``` + +### Important Notes + +1. This action works only when used in a workflow triggered by a Design Review, + as it will automatically pick up the associated design review. +2. By default, successive runs of the action will edit the same comment. +3. This action also reads YAML frontmatter from the markdown file to post + attachments to the posted comment. + +### Customizing the Comment Content + +The action uses a markdown file as the source of the comment body. You can +create a markdown file in your repository and specify its path using the +`comment_path` input. + +Example `comment.md`: + +```markdown +--- +attachments: + - path/to/attachment1.png + - path/to/attachment2.pdf +--- + +# Comment Title + +This is the body of the comment. + +- Point 1 +- Point 2 +- Point 3 + +[Link to more information](https://example.com) +``` + +The YAML frontmatter at the beginning of the file (between `---`) can be used +to specify attachments that will be added to the comment. The YAML frontmatter +is optional, and when present, isn't included in the posted comment's body. + +### Reusing Existing Comments + +By default, the action will reuse the existing comment made by this action in +successive runs. This behavior can be controlled using the +`reuse_existing_comment` input. Set it to 'False' if you want to create a new +comment on each run. + +### Debugging + +If you encounter any issues or need more detailed information about the +action's execution, you can set the `log_level` input to 'DEBUG' for more +verbose logging. + +## SSL + +If your instance is running on a self-signed certificate, you can tell the +action to use your certificate by setting the `REQUESTS_CA_BUNDLE` environment +variable. + +```yaml +- name: Post Comment on Design Review + uses: https://hub.allspice.io/Actions/post-dr-comment@v0.1 + with: + comment_path: path/to/comment.md + env: + REQUESTS_CA_BUNDLE: /path/to/your/certificate.cert +``` + +For more information about AllSpice Actions and how to use them in your +workflows, please refer to the +[AllSpice Documentation](https://learn.allspice.io/docs/actions-cicd). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..24aa129 --- /dev/null +++ b/action.yml @@ -0,0 +1,43 @@ +name: "Post Comment on a Design Review" +description: > + Post a comment on a Design Review using a markdown file as the source of the + comment. + + This works only when used in a workflow triggered by a Design Review, as it + will automatically pick up the associated design review. By default, + successive runs of the action will edit the same comment. + + This action also reads YAML frontmatter from the markdown file to post + attachments to the posted comment. + +inputs: + comment_path: + description: The path to a markdown file containing the comment body. + required: true + reuse_existing_comment: + description: Whether to reuse the existing comment made by this action in successive runs. + required: false + default: "True" + log_level: + description: The log level used by the action. Used for debugging. + required: false + default: "INFO" + +runs: + using: "docker" + image: "Dockerfile" + args: + - "--allspice_hub_url" + - ${{ github.server_url }} + - "--repository" + - ${{ github.repository }} + - "--design-review-number" + - ${{ github.event.number }} + - "--comment-path" + - "${{ github.workspace}}/${{ inputs.comment_path }}" + - "--reuse-existing-comment" + - ${{ inputs.reuse_existing_comment }} + - "--log-level" + - ${{ inputs.log_level }} + env: + ALLSPICE_AUTH_TOKEN: ${{ github.token }} diff --git a/post_dr_comment.py b/post_dr_comment.py new file mode 100644 index 0000000..2452289 --- /dev/null +++ b/post_dr_comment.py @@ -0,0 +1,191 @@ +""" +post_dr_comment.py: Post a comment to an AllSpice Hub Design Review. +""" + +import argparse +import logging +import os +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 + + return front_matter, stripped_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() + + 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 = comment_body + existing_comment.commit() + + return existing_comment + else: + logger.info("Creating new comment.") + return design_review.create_comment(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: + comment.create_attachment(f) + + +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() + + 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() diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..bf1318e --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1 @@ +ruff==0.6.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f829b21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +py-allspice~=3.5.0 +PyYAML~=6.0.2 +