From 34b717acba736b4d5d58cf5cd2ff40a3df02b658 Mon Sep 17 00:00:00 2001 From: Ajesh Sen Thapa Date: Mon, 22 Jan 2024 20:15:37 +0545 Subject: [PATCH] feat: added support for github actions --- .github/workflows/test_action.yaml | 18 ++++++ README.md | 65 +++++++++++++++----- action.yml | 43 +++++++++++++ github_actions/event.py | 98 ++++++++++++++++++++++++++++++ github_actions/run.py | 97 +++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/test_action.yaml create mode 100644 action.yml create mode 100644 github_actions/event.py create mode 100644 github_actions/run.py diff --git a/.github/workflows/test_action.yaml b/.github/workflows/test_action.yaml new file mode 100644 index 0000000..8b9d252 --- /dev/null +++ b/.github/workflows/test_action.yaml @@ -0,0 +1,18 @@ +name: Commitlint + +on: + push: + branches: ["main"] + pull_request: + +jobs: + commitlint: + runs-on: ubuntu-latest + name: Check commit messages + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run commitlint + uses: ./ # Uses an action in the root directory + # or use a released GitHub Action + # uses: opensource-nepal/commitlint@0.1.0 diff --git a/README.md b/README.md index 34b6f87..1c7f131 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,67 @@ # commitlint -commitlint is is a pre-commit hook designed to lint your commit messages according to the [Conventional Commits](https://www.conventionalcommits.org/) standard. +commitlint is a tool designed to lint your commit messages according to the [Conventional Commits](https://www.conventionalcommits.org/) standard for your pre-commit hook and GitHub Actions. ## How to use +### For pre-commit + 1. Add the following configuration on `.pre-commit-config.yaml`. - ```yaml - repos: - ... + ```yaml + repos: + ... - - repo: https://github.com/opensource-nepal/commitlint - rev: 0.1.0 - hooks: - - id: commitlint + - repo: https://github.com/opensource-nepal/commitlint + rev: 0.1.0 + hooks: + - id: commitlint - ... - ``` + ... + ``` 2. Install the `commit-msg` hook in your project repo: - ```bash - pre-commit install --hook-type commit-msg - ``` + ```bash + pre-commit install --hook-type commit-msg + ``` + +> **_NOTE:_** Installing using only `pre-commit install` will not work. + +### For github-actions + +If you have any existing workflows, add the following steps: + +```yaml +steps: + ... + - name: Run commitlint + uses: opensource-nepal/commitlint@0.1.0 + ... +``` + +If you don't have any workflows, create a new GitHub workflow, e.g. `.github/workflows/commitlint.yaml`. + +```yaml +name: Commitlint + +on: + push: + branches: ['main'] + pull_request: + +jobs: + commitlint: + runs-on: ubuntu-latest + name: Check commit messages + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run commitlint + uses: opensource-nepal/commitlint@0.1.0 +``` -> **_NOTE:_** Installing just using `pre-commit install` will not work. +> **_NOTE:_** commitlint GitHub Actions will only be triggered by "push" or "pull_request" events. ## Contribution diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..a47ee32 --- /dev/null +++ b/action.yml @@ -0,0 +1,43 @@ +name: "commitlint" +description: "A GitHub Action to check conventional commit message" +runs: + using: "composite" + steps: + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: "3.8" + + - name: Install Commitlint + run: python -m pip install -e ${{ github.action_path }} + shell: bash + + # checkout to the source code + # for push event + - name: Get pushed commit count + if: github.event_name == 'push' + id: push_commit_count + run: | + echo "count=$(echo '${{ toJson(github.event.commits) }}' | jq '. | length')" \ + >> $GITHUB_OUTPUT + shell: bash + - name: Checkout to pushed commits + if: github.event_name == 'push' + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + fetch-depth: ${{ steps.push_commit_count.outputs.count }} + + # for pull_request event + - name: Checkout to PR source branch + if: github.event_name == 'pull_request' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: ${{ github.event.pull_request.commits }} + + # checking the commits (for both push and pull_request) + - name: Check the commits + id: commitlint + run: python ${{ github.action_path }}/github_actions/run.py + shell: bash diff --git a/github_actions/event.py b/github_actions/event.py new file mode 100644 index 0000000..4546806 --- /dev/null +++ b/github_actions/event.py @@ -0,0 +1,98 @@ +""" +This module defines the `GithubEvent` class for handling GitHub event details. + +Note: + This module relies on the presence of specific environment variables + set by GitHub Actions. +""" +import json +import os +from typing import Any, Dict + + +# pylint: disable=R0902; Too many instance attributes +class GithubEvent: + """Class representing GitHub events. + + This class provides methods for loading and accessing various details of + GitHub events. + + Attributes: + event_name (str): The name of the GitHub event. + sha (str): The commit SHA associated with the event. + ref (str): The Git reference (branch or tag) for the event. + workflow (str): The name of the GitHub workflow. + action (str): The action that triggered the event. + actor (str): The GitHub username of the user or app that triggered the event. + job (str): The name of the job associated with the event. + run_attempt (str): The current attempt number for the job run. + run_number (str): The unique number assigned to the run by GitHub. + run_id (str): The unique identifier for the run. + + event_path (str): The path to the file containing the GitHub event payload. + payload (dict): The GitHub event payload. + + Raises: + EnvironmentError: If the required environment variable 'GITHUB_EVENT_PATH' + is not found. + + Example: + ```python + github_event = GithubEvent() + print(github_event.event_name) + print(github_event.sha) + print(github_event.payload) + ``` + """ + + def __init__(self) -> None: + """Initialize a new instance of the GithubEvent class.""" + self.__load_details() + + def __load_details(self) -> None: + """ + Load GitHub event details from environment variables and event payload file. + + This method initializes the instance attributes by reading values from + environment variables set by GitHub Actions and loading the event payload + from a file. + """ + self.event_name = os.environ.get("GITHUB_EVENT_NAME") + self.sha = os.environ.get("GITHUB_SHA") + self.ref = os.environ.get("GITHUB_REF") + self.workflow = os.environ.get("GITHUB_WORKFLOW") + self.action = os.environ.get("GITHUB_ACTION") + self.actor = os.environ.get("GITHUB_ACTOR") + self.job = os.environ.get("GITHUB_JOB") + self.run_attempt = os.environ.get("GITHUB_RUN_ATTEMPT") + self.run_number = os.environ.get("GITHUB_RUN_NUMBER") + self.run_id = os.environ.get("GITHUB_RUN_ID") + + if "GITHUB_EVENT_PATH" not in os.environ: + raise EnvironmentError("GITHUB_EVENT_PATH not found on the environment.") + + self.event_path = os.environ["GITHUB_EVENT_PATH"] + with open(self.event_path, encoding="utf-8") as file: + self.payload = json.load(file) + + def to_dict(self) -> Dict[str, Any]: + """ + Convert the GithubEvent instance to a dictionary. + + Returns: + dict: A dictionary containing the attributes of the GithubEvent instance. + """ + return { + attr: getattr(self, attr) + for attr in dir(self) + if not callable(getattr(self, attr)) and not attr.startswith("__") + } + + def __str__(self) -> str: + """ + Returns string representation of the github event data. + + Returns: + str: Github event data. + """ + return str(self.to_dict()) diff --git a/github_actions/run.py b/github_actions/run.py new file mode 100644 index 0000000..819b43b --- /dev/null +++ b/github_actions/run.py @@ -0,0 +1,97 @@ +""" +This script contains actions to be taken based on GitHub events, +specifically for push and pull_request events. +""" +import subprocess +import sys + +from event import GithubEvent + +EVENT_PUSH = "push" +EVENT_PULL_REQUEST = "pull_request" + + +def _handle_pr_event(event: GithubEvent) -> None: + """ + Handle pull_request GitHub event. + + Args: + event (GithubEvent): An instance of the GithubEvent class representing + the GitHub event. + + Raises: + EnvironmentError: If the base SHA and head SHA cannot be retrieved from + the event payload. + """ + try: + to_commit = event.payload["pull_request"]["head"]["sha"] + + # getting from_commit using the total commits count + _total_commits = event.payload["pull_request"]["commits"] + from_commit = f"{to_commit}~{_total_commits-1}" + _check_commits(from_commit, to_commit) + except KeyError: + raise EnvironmentError("Unable to retrieve Base SHA and Head SHA") from None + + +def _handle_push_event(event: GithubEvent) -> None: + """ + Handle push GitHub event. + + Args: + event (GithubEvent): An instance of the GithubEvent class representing + the GitHub event. + + Raises: + EnvironmentError: If the from hash and to hash cannot be retrieved from + the event payload. + """ + try: + commits = event.payload["commits"] + from_commit = commits[0]["id"] + to_commit = commits[-1]["id"] + _check_commits(from_commit, to_commit) + except KeyError: + raise EnvironmentError("Unable to retrieve From hash and To hash") from None + + +def _check_commits(from_hash: str, to_hash: str) -> None: + """Check commits using commitlint. + + Args: + from_hash (str): The hash of the starting commit. + to_hash (str): The hash of the ending commit. + """ + sys.stdout.write(f"Commit from {from_hash} to {to_hash}\n") + try: + output = subprocess.check_output( + [ + "commitlint", + "--from-hash", + from_hash, + "--to-hash", + to_hash, + ], + text=True, + ).strip() + sys.stdout.write(f"{output}\n") + except subprocess.CalledProcessError: + sys.exit(1) + + +def main() -> None: + """Main entry point for the GitHub Actions workflow.""" + event = GithubEvent() + + if event.event_name == EVENT_PUSH: + _handle_push_event(event) + elif event.event_name == EVENT_PULL_REQUEST: + _handle_pr_event(event) + elif event.event_name is None: + sys.stdout.write("No any events, skipping\n") + else: + sys.stdout.write(f"Skipping for event {event.event_name}\n") + + +if __name__ == "__main__": + main()