diff --git a/.commit-check.yml b/.commit-check.yml index bfabe35..f1f725f 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -7,19 +7,24 @@ checks: [optional body]\n [optional footer(s)]\n\n More details please refer to https://www.conventionalcommits.org" - suggest: git commit --amend --no-verify + suggest: please check your commit message whether matches above regex - check: branch - regex: ^(bugfix|feature|release|hotfix|task|dependabot)\/.+|(master)|(main)|(HEAD)|(PR-.+) - error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/" - suggest: git checkout -b type/branch_name + regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) + error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/" + suggest: run command `git checkout -b type/branch_name` - check: author_name - regex: ^[A-Za-z ,.\'-]+$|.*(\[bot]) + regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) error: The committer name seems invalid - suggest: git config user.name "Peter Shen" + suggest: run command `git config user.name "Your Name"` - check: author_email - regex: ^\S+@\S+\.\S+$ + regex: ^.+@.+$ error: The committer email seems invalid - suggest: git config user.email petershen@example.com + suggest: run command `git config user.email yourname@example.com` + + - check: merge_base + regex: main # it can be master, develop, devel etc based on your project. + error: Current branch is not rebased onto target branch + suggest: please ensure your branch is rebased with the target branch diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index 9689bb3..dc398fd 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -1,7 +1,6 @@ name: Commit Check on: - push: pull_request: branches: 'main' workflow_dispatch: @@ -9,13 +8,23 @@ on: jobs: commit-check: runs-on: ubuntu-latest + permissions: # use permissions because of use pr-comments + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 - - uses: ./ # self test + with: + ref: ${{ github.event.pull_request.head.sha }} # checkout PR HEAD commit + fetch-depth: 0 # fetch all history for all branches and tags + - uses: ./ # self test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # use GITHUB_TOKEN because of use pr-comments with: message: true branch: true author-name: true author-email: true commit-signoff: true + merge-base: true job-summary: true + pr-comments: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..7ee1b19 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,10 @@ +name: Run pre-commit + +on: + push: + pull_request: + types: opened + +jobs: + pre-commit: + uses: commit-check/.github/.github/workflows/pre-commit.yml@main diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index c6536f9..d25c13e 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,10 +7,5 @@ on: workflow_dispatch: jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - # Drafts your next Release notes as Pull Requests are merged into the default branch - - uses: release-drafter/release-drafter@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + draft-release: + uses: commit-check/.github/.github/workflows/release-drafter.yml@main diff --git a/.github/workflows/used-by.yml b/.github/workflows/used-by.yml index ea70dc8..0625b49 100644 --- a/.github/workflows/used-by.yml +++ b/.github/workflows/used-by.yml @@ -6,18 +6,22 @@ on: - cron: '0 9 * * 1' # At 09:00 on Monday. workflow_dispatch: +permissions: + pull-requests: write + contents: write + jobs: used-by: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: shenxianpeng/used-by@v0.1.2 + - uses: shenxianpeng/used-by@v0.1.4 with: repo: '${{ github.repository }}' update-badge: 'true' - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: add-paths: "README.md" # the file path to commit commit-message: "chore: update used-by badge by github-actions[bot]" diff --git a/.gitignore b/.gitignore index f7275bb..43f4b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ venv/ +.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..571d5ee --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# https://pre-commit.com/ +ci: + autofix_commit_msg: 'ci: auto fixes from pre-commit.com hooks' + autoupdate_commit_msg: 'ci: pre-commit autoupdate' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: name-tests-test + - id: requirements-txt-fixer +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.10.0 + hooks: + - id: black +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.12.0 + hooks: + - id: mypy +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell diff --git a/README.md b/README.md index b655631..b1581f8 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,20 @@ [![Main](https://github.com/commit-check/commit-check-action/actions/workflows/main.yaml/badge.svg)](https://github.com/commit-check/commit-check-action/actions/workflows/main.yaml) [![Commit Check](https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml/badge.svg)](https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/commit-check/commit-check-action) -[![Used by](https://img.shields.io/static/v1?label=Used%20by&message=13&color=informational&logo=slickpic)](https://github.com/commit-check/commit-check-action/network/dependents) +[![Used by](https://img.shields.io/static/v1?label=Used%20by&message=62&color=informational&logo=slickpic)](https://github.com/commit-check/commit-check-action/network/dependents) [![GitHub marketplace](https://img.shields.io/badge/Marketplace-commit--check--action-blue)](https://github.com/marketplace/actions/commit-check-action) A Github Action for checking commit message formatting, branch naming, committer name, email, commit signoff and more. +## Table of Contents + +* [Usage](#usage) +* [Optional Inputs](#optional-inputs) +* [GitHub Action Job Summary](#github-action-job-summary) +* [GitHub Pull Request Comments](#github-pull-request-comments) +* [Badging Your Repository](#badging-your-repository) +* [Versioning](#versioning) + ## Usage Create a new GitHub Actions workflow in your project, e.g. at [.github/workflows/commit-check.yml](.github/workflows/commit-check.yml) @@ -23,75 +32,130 @@ on: jobs: commit-check: runs-on: ubuntu-latest + permissions: # use permissions because of use pr-comments + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} # checkout PR HEAD commit + fetch-depth: 0 # required for merge-base check - uses: commit-check/commit-check-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # use GITHUB_TOKEN because of use pr-comments with: message: true branch: true author-name: true author-email: true commit-signoff: true - dry-run: true + merge-base: false job-summary: true + pr-comments: ${{ github.event_name == 'pull_request' }} ``` +## Used By + +

+ Apache + Apache   + discovery-unicamp + discovery-unicamp   + Texas Instruments + Texas Instruments   + OpenCADC + OpenCADC   + Extrawest + Extrawest + and many more. +

+ ## Optional Inputs ### `message` - **Description**: check commit message formatting convention. - By default the rule follows [conventional commits](https://www.conventionalcommits.org/). -- Default: 'true' +- Default: `true` ### `branch` - **Description**: check git branch naming convention. - - By default follow bitbucket [branching model](https://support.atlassian.com/bitbucket-cloud/docs/configure-a-projects-branching-model/). -- Default: 'true' + - By default the rule follows [conventional branch](https://conventional-branch.github.io/). +- Default: `true` ### `author-name` -- **Description**: check committer author name -- Default: 'true' +- **Description**: check committer author name. +- Default: `true` ### `author-email` -- **Description**: check committer author email -- Default: 'true' +- **Description**: check committer author email. +- Default: `true` ### `commit-signoff` -- **Description**: check committer commit signature -- Default: 'true' +- **Description**: check committer commit signature. +- Default: `true` + +### `merge-base` + +- **Description**: check current branch is rebased onto target branch. +- Default: `false` + +> [!IMPORTANT] +> `merge-base` is an experimental feature. by default it's disable. +> +> To use this feature, you need fetch all history for all branches by setting `fetch-depth: 0` in `actions/checkout`. ### `dry-run` - **Description**: run checks without failing. exit code is 0 otherwise is 1. -- Default: 'false' +- Default: `false` ### `job-summary` -- **Description**: display job summary to a workflow run -- Default: 'true' +- **Description**: display job summary to the workflow run. +- Default: `true` + +### `pr-comments` + +- **Description**: post results to the pull request comments. +- Default: `false` + +> [!IMPORTANT] +> `pr-comments` is an experimental feature. by default it's disable. To use it you need to set `GITHUB_TOKEN` in the GitHub Action. +> +> This feature currently doesn’t work with forked repositories. For more details, refer to issue [#77](https://github.com/commit-check/commit-check-action/issues/77). Note: the default rule of above inputs is following [this configuration](https://github.com/commit-check/commit-check/blob/main/.commit-check.yml), if you want to customize just add your `.commit-check.yml` config file under your repository root directory. -## GitHub Action job summary +## GitHub Action Job Summary + +By default, commit-check-action results are shown on the job summary page of the workflow. + +### Success Job Summary + +![Success job summary](https://github.com/commit-check/.github/blob/main/screenshot/success-job-summary.png) + +### Failure Job Summary + +![Failure job summary](https://github.com/commit-check/.github/blob/main/screenshot/failure-job-summary.png) -By default, commit-check-action results are shown on the job summary page of the workflow. +## GitHub Pull Request Comments -### Success job summary +### Success Pull Request Comment -![Success job summary](https://github.com/commit-check/.github/blob/main/screenshot/success-summary.png) +![Success pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/success-pr-comments.png) -### Failure job summary +### Failure Pull Request Comment -![Failure job summary](https://github.com/commit-check/.github/blob/main/screenshot/failure-summary.png) +![Failure pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/failure-pr-comments.png) -## Badging your repository +## Badging Your Repository -You can add a badge to your repository to show your contributors / users that you use commit-check! +You can add a badge to your repository to show your contributors/users that you use commit-check! [![Commit Check](https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml/badge.svg)](https://github.com/commit-check/commit-check-action/actions/workflows/commit-check.yml) @@ -114,6 +178,6 @@ reStructuredText Versioning follows [Semantic Versioning](https://semver.org/). -## Have question or feedback? +## Have questions or feedback? To provide feedback (requesting a feature or reporting a bug) please post to [issues](https://github.com/commit-check/commit-check/issues). diff --git a/action.yml b/action.yml index 7a1823d..cdd75ea 100644 --- a/action.yml +++ b/action.yml @@ -25,33 +25,57 @@ inputs: description: check committer commit signature required: false default: true + merge-base: + description: check current branch is rebased onto target branch + required: false + default: false dry-run: description: run checks without failing required: false default: false job-summary: - description: add a job summary + description: display job summary to the workflow run required: false default: true + pr-comments: + description: post results to the pull request comments + required: false + default: false runs: using: "composite" steps: - - name: Install action dependencies + - name: Install dependencies and run commit-check shell: bash run: | if [[ "$RUNNER_OS" == "Linux" ]]; then # https://github.com/pypa/setuptools/issues/3269 export DEB_PYTHON_INSTALL_LAYOUT=deb fi - python3 -m pip install -r "$GITHUB_ACTION_PATH/requirements.txt" - - name: Run commit-check - shell: bash - run: python3 ${{ github.action_path }}/main.py + + # Set up virtual environment + python3 -m venv venv + source venv/bin/activate + + # Download artifact + python3 -m pip download -r "$GITHUB_ACTION_PATH/requirements.txt" + + # Verify artifact attestations + if ! gh attestation verify commit_check-*.whl -R commit-check/commit-check; then + echo "Artifact verification failed. Aborting installation." + exit 1 + fi + + # Install artifact + python3 -m pip install commit_check-*.whl PyGithub-*.whl + + python3 "$GITHUB_ACTION_PATH/main.py" env: MESSAGE: ${{ inputs.message }} BRANCH: ${{ inputs.branch }} AUTHOR_NAME: ${{ inputs.author-name }} AUTHOR_EMAIL: ${{ inputs.author-email }} COMMIT_SIGNOFF: ${{ inputs.commit-signoff }} + MERGE_BASE: ${{ inputs.merge-base }} DRY_RUN: ${{ inputs.dry-run }} JOB_SUMMARY: ${{ inputs.job-summary }} + PR_COMMENTS: ${{ inputs.pr-comments }} diff --git a/main.py b/main.py index 2b07aec..87434ec 100755 --- a/main.py +++ b/main.py @@ -3,59 +3,198 @@ import sys import subprocess import re +from github import Github # type: ignore + + +# Constants for message titles +SUCCESS_TITLE = "# Commit-Check ✔️" +FAILURE_TITLE = "# Commit-Check ❌" + +# Environment variables +MESSAGE = os.getenv("MESSAGE", "false") +BRANCH = os.getenv("BRANCH", "false") +AUTHOR_NAME = os.getenv("AUTHOR_NAME", "false") +AUTHOR_EMAIL = os.getenv("AUTHOR_EMAIL", "false") +COMMIT_SIGNOFF = os.getenv("COMMIT_SIGNOFF", "false") +MERGE_BASE = os.getenv("MERGE_BASE", "false") +DRY_RUN = os.getenv("DRY_RUN", "false") +JOB_SUMMARY = os.getenv("JOB_SUMMARY", "false") +PR_COMMENTS = os.getenv("PR_COMMENTS", "false") +GITHUB_STEP_SUMMARY = os.environ["GITHUB_STEP_SUMMARY"] +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") +GITHUB_REF = os.getenv("GITHUB_REF") + + +def log_env_vars(): + """Logs the environment variables for debugging purposes.""" + print(f"MESSAGE = {MESSAGE}") + print(f"BRANCH = {BRANCH}") + print(f"AUTHOR_NAME = {AUTHOR_NAME}") + print(f"AUTHOR_EMAIL = {AUTHOR_EMAIL}") + print(f"COMMIT_SIGNOFF = {COMMIT_SIGNOFF}") + print(f"MERGE_BASE = {MERGE_BASE}") + print(f"DRY_RUN = {DRY_RUN}") + print(f"JOB_SUMMARY = {JOB_SUMMARY}") + print(f"PR_COMMENTS = {PR_COMMENTS}\n") def run_commit_check() -> int: - args = ["--message", "--branch", "--author-name", "--author-email", "--commit-signoff"] - args = [arg for arg, value in zip(args, [MESSAGE, BRANCH, AUTHOR_NAME, AUTHOR_EMAIL, COMMIT_SIGNOFF]) if value == "true"] + """Runs the commit-check command and logs the result.""" + args = [ + "--message", + "--branch", + "--author-name", + "--author-email", + "--commit-signoff", + "--merge-base", + ] + args = [ + arg + for arg, value in zip( + args, + [MESSAGE, BRANCH, AUTHOR_NAME, AUTHOR_EMAIL, COMMIT_SIGNOFF, MERGE_BASE], + ) + if value == "true" + ] command = ["commit-check"] + args print(" ".join(command)) with open("result.txt", "w") as result_file: - result = subprocess.run(command, stdout=result_file, stderr=subprocess.PIPE, check=False) + result = subprocess.run( + command, stdout=result_file, stderr=subprocess.PIPE, check=False + ) return result.returncode +def read_result_file() -> str | None: + """Reads the result.txt file and removes ANSI color codes.""" + if os.path.getsize("result.txt") > 0: + with open("result.txt", "r") as result_file: + result_text = re.sub( + r"\x1B\[[0-9;]*[a-zA-Z]", "", result_file.read() + ) # Remove ANSI colors + return result_text.rstrip() + return None + + def add_job_summary() -> int: + """Adds the commit check result to the GitHub job summary.""" if JOB_SUMMARY == "false": - sys.exit() + return 0 - if os.path.getsize("result.txt") > 0: - with open("result.txt", "r") as result_file: - result_text = re.sub(r'\x1B\[[0-9;]*[a-zA-Z]', '', result_file.read()) # Remove ANSI colors + result_text = read_result_file() - with open(GITHUB_STEP_SUMMARY, "a") as summary_file: - summary_file.write("### Commit-Check ❌\n```\n") - summary_file.write(result_text) - summary_file.write("```") - return 1 - else: - with open(GITHUB_STEP_SUMMARY, "a") as summary_file: - summary_file.write("### Commit-Check ✔️\n") + summary_content = ( + SUCCESS_TITLE + if result_text is None + else f"{FAILURE_TITLE}\n```\n{result_text}\n```" + ) + + with open(GITHUB_STEP_SUMMARY, "a") as summary_file: + summary_file.write(summary_content) + + return 0 if result_text is None else 1 + + +def add_pr_comments() -> int: + """Posts the commit check result as a comment on the pull request.""" + if PR_COMMENTS == "false": return 0 + try: + token = os.getenv("GITHUB_TOKEN") + repo_name = os.getenv("GITHUB_REPOSITORY") + pr_number = os.getenv("GITHUB_REF") + if pr_number is not None: + pr_number = pr_number.split("/")[-2] + else: + # Handle the case where GITHUB_REF is not set + raise ValueError("GITHUB_REF environment variable is not set") -MESSAGE = os.getenv("MESSAGE", "false") -BRANCH = os.getenv("BRANCH", "false") -AUTHOR_NAME = os.getenv("AUTHOR_NAME", "false") -AUTHOR_EMAIL = os.getenv("AUTHOR_EMAIL", "false") -COMMIT_SIGNOFF = os.getenv("COMMIT_SIGNOFF", "false") -DRY_RUN = os.getenv("DRY_RUN", "false") -JOB_SUMMARY = os.getenv("JOB_SUMMARY", "false") -GITHUB_STEP_SUMMARY = os.environ["GITHUB_STEP_SUMMARY"] + # Initialize GitHub client + g = Github(token) + repo = g.get_repo(repo_name) + pull_request = repo.get_issue(int(pr_number)) + + # Prepare comment content + result_text = read_result_file() + pr_comments = ( + SUCCESS_TITLE + if result_text is None + else f"{FAILURE_TITLE}\n```\n{result_text}\n```" + ) + + # Fetch all existing comments on the PR + comments = pull_request.get_comments() + + # Track if we found a matching comment + matching_comments = [] + last_comment = None + + for comment in comments: + if comment.body.startswith(SUCCESS_TITLE) or comment.body.startswith( + FAILURE_TITLE + ): + matching_comments.append(comment) + if matching_comments: + last_comment = matching_comments[-1] + + if last_comment.body == pr_comments: + print(f"PR comment already up-to-date for PR #{pr_number}.") + return 0 + else: + # If the last comment doesn't match, update it + print(f"Updating the last comment on PR #{pr_number}.") + last_comment.edit(pr_comments) + + # Delete all older matching comments + for comment in matching_comments[:-1]: + print(f"Deleting an old comment on PR #{pr_number}.") + comment.delete() + else: + # No matching comments, create a new one + print(f"Creating a new comment on PR #{pr_number}.") + pull_request.create_comment(body=pr_comments) + + return 0 if result_text is None else 1 + except Exception as e: + print(f"Error posting PR comment: {e}", file=sys.stderr) + return 1 + + +def log_error_and_exit( + failure_title: str, result_text: str | None, ret_code: int +) -> None: + """ + Logs an error message to GitHub Actions and exits with the specified return code. + + Args: + failure_title (str): The title of the failure message. + result_text (str): The detailed result text to include in the error message. + ret_code (int): The return code to exit with. + """ + if result_text: + error_message = f"{failure_title}\n```\n{result_text}\n```" + print(f"::error::{error_message}") + sys.exit(ret_code) + + +def main(): + """Main function to run commit-check, add job summary and post PR comments.""" + log_env_vars() + + # Combine return codes + ret_code = run_commit_check() + ret_code += add_job_summary() + ret_code += add_pr_comments() -print(f"MESSAGE = {MESSAGE}") -print(f"BRANCH = {BRANCH}") -print(f"AUTHOR_NAME = {AUTHOR_NAME}") -print(f"AUTHOR_EMAIL = {AUTHOR_EMAIL}") -print(f"COMMIT_SIGNOFF = {COMMIT_SIGNOFF}") -print(f"DRY_RUN = {DRY_RUN}") -print(f"JOB_SUMMARY = {JOB_SUMMARY}\n") + if DRY_RUN == "true": + ret_code = 0 -ret_code = run_commit_check() -ret_code += add_job_summary() # Combine return codes + result_text = read_result_file() + log_error_and_exit(FAILURE_TITLE, result_text, ret_code) -if DRY_RUN == "true": - ret_code = 0 -sys.exit(ret_code) +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 5c0c2fb..764bb33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ # Install commit-check CLI # For details please see: https://github.com/commit-check/commit-check -commit-check==0.7.3 +commit-check==0.9.5 +# Interact with the GitHub API. +PyGithub==2.6.1