From 4132c0da6888d051eb7574fcbc841c32e113964b Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:14:24 +0000 Subject: [PATCH 01/11] feat: support add pull request comments * add .pre-commit-config.yaml to format code --- .pre-commit-config.yaml | 28 ++++++++ README.md | 10 ++- action.yml | 7 +- main.py | 156 +++++++++++++++++++++++++++++++--------- requirements.txt | 2 + 5 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b45ec09 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# 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 +# FIXME: main.py:109: error: Item "None" of "str | None" has no attribute "split" [union-attr] +# - 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 426ff0e..6a84c9c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ jobs: commit-signoff: true dry-run: true job-summary: true + pr-comments: true ``` ## Optional Inputs @@ -72,14 +73,19 @@ jobs: ### `job-summary` -- **Description**: display job summary to a workflow run +- **Description**: display job summary to the workflow run +- Default: 'true' + +### `pr-comments` + +- **Description**: post results to the pull request comments - Default: 'true' 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 -By default, commit-check-action results are shown on the job summary page of the workflow. +By default, commit-check-action results are shown on the job summary page of the workflow. ### Success job summary diff --git a/action.yml b/action.yml index 833ba66..531acb4 100644 --- a/action.yml +++ b/action.yml @@ -30,9 +30,13 @@ inputs: 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: @@ -55,3 +59,4 @@ runs: COMMIT_SIGNOFF: ${{ inputs.commit-signoff }} 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..3a905f4 100755 --- a/main.py +++ b/main.py @@ -3,59 +3,145 @@ import sys import subprocess import re +from github import Github + + +# Constants for message titles +SUCCESS_TITLE = "### Commit-Check ✔️\n" +FAILURE_TITLE = "### Commit-Check ❌\n" + +# 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") +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"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", + ] + args = [ + arg + for arg, value in zip( + args, [MESSAGE, BRANCH, AUTHOR_NAME, AUTHOR_EMAIL, COMMIT_SIGNOFF] + ) + 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 + 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{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" + or not GITHUB_TOKEN + or not GITHUB_REPOSITORY + or not GITHUB_REF + ): return 0 + try: + token = os.getenv("GITHUB_TOKEN") + repo_name = os.getenv("GITHUB_REPOSITORY") + pr_number = os.getenv("GITHUB_REF").split("/")[-2] -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) + issue = 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{result_text}\n```" + ) + + issue.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 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 + sys.exit(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 cd2611c..25fef9f 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.8.3 +# Interact with the GitHub API. +PyGithub=v2.4.0 From 534a50dab332bbb032c5b09782256d55ccbbc2c8 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:17:07 +0000 Subject: [PATCH 02/11] fix: requirements.txt and test new input --- .github/workflows/commit-check.yml | 1 + requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index 967657d..5ee3d34 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -18,3 +18,4 @@ jobs: author-email: true commit-signoff: true job-summary: true + pr-comments: true diff --git a/requirements.txt b/requirements.txt index 25fef9f..8a6ff3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ # For details please see: https://github.com/commit-check/commit-check commit-check==0.8.3 # Interact with the GitHub API. -PyGithub=v2.4.0 +PyGithub==2.4.0 From be702afc293786e2f7ee907c2578b3ac924d08c8 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:21:03 +0000 Subject: [PATCH 03/11] fix: just checking PR_COMMENTS --- main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/main.py b/main.py index 3a905f4..07ae14e 100755 --- a/main.py +++ b/main.py @@ -95,12 +95,7 @@ def add_job_summary() -> int: def add_pr_comments() -> int: """Posts the commit check result as a comment on the pull request.""" - if ( - PR_COMMENTS == "false" - or not GITHUB_TOKEN - or not GITHUB_REPOSITORY - or not GITHUB_REF - ): + if PR_COMMENTS == "false": return 0 try: From 4b588d18404d0f91f0b476fd180fc38736889d20 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:29:48 +0000 Subject: [PATCH 04/11] feat: add GITHUB_TOKEN --- .github/workflows/commit-check.yml | 2 ++ .gitignore | 1 + main.py | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index 5ee3d34..61da88e 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./ # self test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # used by `pr-comments` with: message: true branch: true diff --git a/.gitignore b/.gitignore index f7275bb..43f4b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ venv/ +.venv/ diff --git a/main.py b/main.py index 07ae14e..cead20e 100755 --- a/main.py +++ b/main.py @@ -106,7 +106,7 @@ def add_pr_comments() -> int: # Initialize GitHub client g = Github(token) repo = g.get_repo(repo_name) - issue = repo.get_issue(int(pr_number)) + pr = repo.get_issue(int(pr_number)) # Prepare comment content result_text = read_result_file() @@ -116,7 +116,7 @@ def add_pr_comments() -> int: else f"{FAILURE_TITLE}```\n{result_text}\n```" ) - issue.create_comment(body=pr_comments) + pr.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) From b74de65c56c7d756b8a7d613230239f506d272b7 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:38:59 +0000 Subject: [PATCH 05/11] update readme.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6a84c9c..073d960 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ jobs: - **Description**: post results to the pull request comments - Default: 'true' +> [!IMPORTANT] +> This is a experimental feature +> use it you need to set `GITHUB_TOKEN` in the GitHub Action. + 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 @@ -95,6 +99,12 @@ By default, commit-check-action results are shown on the job summary page of the ![Failure job summary](https://github.com/commit-check/.github/blob/main/screenshot/failure-summary.png) +## GitHub Pull Request comments + +### Success pull request comment + +![Success pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/success-pr-comments.png) + ## Badging your repository You can add a badge to your repository to show your contributors / users that you use commit-check! From f5376d98339eb252ab9ec76f56220c413c3ac7e6 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:48:18 +0000 Subject: [PATCH 06/11] update exiting comments or delete it to add new one --- main.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index cead20e..dd6bf11 100755 --- a/main.py +++ b/main.py @@ -106,7 +106,7 @@ def add_pr_comments() -> int: # Initialize GitHub client g = Github(token) repo = g.get_repo(repo_name) - pr = repo.get_issue(int(pr_number)) + pull_request = repo.get_issue(int(pr_number)) # Prepare comment content result_text = read_result_file() @@ -116,7 +116,38 @@ def add_pr_comments() -> int: else f"{FAILURE_TITLE}```\n{result_text}\n```" ) - pr.create_comment(body=pr_comments) + # Fetch all existing comments on the PR + comments = pull_request.get_issue_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_issue_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) From d8d5c77f567819771248ca69f5a645215d35f9a0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:51:22 +0000 Subject: [PATCH 07/11] try to fix 'Issue' object has no attribute 'get_issue_comments' --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index dd6bf11..362fe6a 100755 --- a/main.py +++ b/main.py @@ -117,7 +117,7 @@ def add_pr_comments() -> int: ) # Fetch all existing comments on the PR - comments = pull_request.get_issue_comments() + comments = pull_request.get_comments() # Track if we found a matching comment matching_comments = [] From b7b4b677bebc043f7e2a720dbcc418736bcc6904 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 10:56:10 +0000 Subject: [PATCH 08/11] update .commit-check.yml --- .commit-check.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.commit-check.yml b/.commit-check.yml index bfabe35..15ac7e4 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -7,19 +7,19 @@ 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]) 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+$ error: The committer email seems invalid - suggest: git config user.email petershen@example.com + suggest: run command `git config user.email yourname@example.com` From 6d6d495388091f9fb6b7a9d086dd251c79eb2b7c Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 18 Oct 2024 11:13:45 +0000 Subject: [PATCH 09/11] try to fix posting issue comment --- .github/workflows/commit-check.yml | 2 ++ main.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index 61da88e..ecea18c 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - uses: ./ # self test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # used by `pr-comments` diff --git a/main.py b/main.py index 362fe6a..eb0af9c 100755 --- a/main.py +++ b/main.py @@ -7,8 +7,8 @@ # Constants for message titles -SUCCESS_TITLE = "### Commit-Check ✔️\n" -FAILURE_TITLE = "### Commit-Check ❌\n" +SUCCESS_TITLE = "# Commit-Check ✔️" +FAILURE_TITLE = "# Commit-Check ❌" # Environment variables MESSAGE = os.getenv("MESSAGE", "false") @@ -70,7 +70,7 @@ def read_result_file() -> str | None: result_text = re.sub( r"\x1B\[[0-9;]*[a-zA-Z]", "", result_file.read() ) # Remove ANSI colors - return result_text + return result_text.rstrip() return None @@ -84,7 +84,7 @@ def add_job_summary() -> int: summary_content = ( SUCCESS_TITLE if result_text is None - else f"{FAILURE_TITLE}```\n{result_text}\n```" + else f"{FAILURE_TITLE}\n```\n{result_text}\n```" ) with open(GITHUB_STEP_SUMMARY, "a") as summary_file: @@ -113,7 +113,7 @@ def add_pr_comments() -> int: pr_comments = ( SUCCESS_TITLE if result_text is None - else f"{FAILURE_TITLE}```\n{result_text}\n```" + else f"{FAILURE_TITLE}\n```\n{result_text}\n```" ) # Fetch all existing comments on the PR @@ -146,7 +146,7 @@ def add_pr_comments() -> int: else: # No matching comments, create a new one print(f"Creating a new comment on PR #{pr_number}.") - pull_request.create_issue_comment(body=pr_comments) + pull_request.create_comment(body=pr_comments) return 0 if result_text is None else 1 except Exception as e: From b8d374f50ff783a96c8b529424ec09f251f10d68 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Tue, 22 Oct 2024 19:32:31 +0000 Subject: [PATCH 10/11] docs: update readme --- README.md | 2 ++ action.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 073d960..ed7bc6c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} # Checkout PR HEAD commit - uses: commit-check/commit-check-action@v1 with: message: true diff --git a/action.yml b/action.yml index 531acb4..804f81c 100644 --- a/action.yml +++ b/action.yml @@ -36,7 +36,7 @@ inputs: pr-comments: description: post results to the pull request comments required: false - default: false + default: true runs: using: "composite" steps: From c3150d0c584acbb739e39a3c6b3bb4790a46258d Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Tue, 22 Oct 2024 19:39:51 +0000 Subject: [PATCH 11/11] docs: update links --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed7bc6c..c9a2679 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,11 @@ By default, commit-check-action results are shown on the job summary page of the ### Success job summary -![Success job summary](https://github.com/commit-check/.github/blob/main/screenshot/success-summary.png) +![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-summary.png) +![Failure job summary](https://github.com/commit-check/.github/blob/main/screenshot/failure-job-summary.png) ## GitHub Pull Request comments @@ -107,6 +107,10 @@ By default, commit-check-action results are shown on the job summary page of the ![Success pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/success-pr-comments.png) +### Failure pull request comment + +![Failure pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/failure-pr-comments.png) + ## Badging your repository You can add a badge to your repository to show your contributors / users that you use commit-check! pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy