@@ -1,179 +1,88 @@
|
||||
# 🧩 PR Commenter for GitHub & Gitea
|
||||
# 🧱 Git Auto Comment Action
|
||||
|
||||
A composite Action that posts PR comments from **large output files**, such as Terraform/OpenTofu plans, logs, or diffs — directly to **GitHub** or **Gitea** pull requests.
|
||||
|
||||
It’s based on [`tofu-pr-commenter`](https://github.com/alexnorell/tofu-pr-commenter) but extended for **Gitea compatibility**, **multi-line comment templates**, and **general-purpose content handling** (not just diffs).
|
||||
Automatically post comments or pull request reviews to **Gitea** or **GitHub**, with optional debug logging and diff parsing.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- ✅ Works with **GitHub** and **Gitea** PR APIs
|
||||
- ✅ Handles **large text outputs** (plans, diffs, logs, etc.)
|
||||
- ✅ Allows **multiline comment templates** with placeholders
|
||||
- ✅ Defaults to environment variables from the Action runner (no hardcoded repo info required)
|
||||
- ✅ Can run on **both GitHub Actions** and **Gitea Actions**
|
||||
- 🧩 Supports **GitHub** and **Gitea**
|
||||
- 💬 Posts formatted PR comments or reviews
|
||||
- 🪶 Uses `{line}` / `{lines}` placeholders in templates
|
||||
- 🧠 Parses diffs to include relevant code changes
|
||||
- 🎨 Optional **colorized debug mode**
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Inputs
|
||||
## 🧰 Inputs
|
||||
|
||||
| Name | Required | Default | Description |
|
||||
|------|-----------|----------|-------------|
|
||||
| `platform` | ❌ | `github` | Platform type (`github` or `gitea`) |
|
||||
| `token` | ✅ | — | Access token for API requests (`GITHUB_TOKEN` or personal token) |
|
||||
| `pr_index` | ✅ | — | Pull request number or index |
|
||||
| `repo_owner` | ❌ | `${{ github.repository_owner }}` | Repository owner |
|
||||
| `repo_name` | ❌ | `${{ github.repository }}` | Repository name |
|
||||
| `api_url` | ❌ | `${{ github.api_url }}` | API URL for Gitea (required only for Gitea) |
|
||||
| `content` | ❌ | — | Large text input — diff, plan, or log content |
|
||||
| `comment_template` | ❌ | See below | Comment body template supporting `{line}` and `{lines}` placeholders |
|
||||
|
||||
### 🧠 Template Variables
|
||||
|
||||
| Placeholder | Description |
|
||||
|--------------|--------------|
|
||||
| `{line}` | The specific line being commented on (when parsed from diff-style input) |
|
||||
| `{lines}` | The full set of added lines for that file or section |
|
||||
| `platform` | No | `github` | Target platform (`github` or `gitea`) |
|
||||
| `token` | ✅ | — | API token |
|
||||
| `repo_owner` | ✅ | — | Repository owner |
|
||||
| `repo_name` | ✅ | — | Repository name |
|
||||
| `pr_index` | ✅ | — | Pull request index (Gitea) or issue number (GitHub) |
|
||||
| `api_url` | No | — | Base API URL (required for Gitea) |
|
||||
| `diff` | No | — | Diff or plan text to include in the comment |
|
||||
| `comment_template` | No | `"Auto-comment: changed line -> {line}"` | Template text. Supports `{line}` and `{lines}` placeholders |
|
||||
| `debug` | No | `false` | Enable verbose, colorized debug logs |
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Default `comment_template`
|
||||
## 🧮 Example Usage
|
||||
|
||||
```yaml
|
||||
comment_template: |
|
||||
Auto-comment:
|
||||
---
|
||||
{line}
|
||||
```
|
||||
name: Auto PR Comment
|
||||
|
||||
## 💡 Example: GitHub Action Workflow
|
||||
```yaml
|
||||
name: Comment Terraform Plan on PR
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
plan-comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Terraform Plan
|
||||
id: plan
|
||||
run: |
|
||||
terraform plan -no-color > plan.txt
|
||||
echo "plan_text<<EOF" >> $GITHUB_OUTPUT
|
||||
cat plan.txt >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment Plan on GitHub PR
|
||||
uses: "https://gitea.example.com/your-org/pr-commenter-action@main"
|
||||
with:
|
||||
platform: github
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_index: ${{ github.event.pull_request.number }}
|
||||
content: ${{ steps.plan.outputs.plan_text }}
|
||||
comment_template: |
|
||||
🚀 **Terraform Plan Summary**
|
||||
```
|
||||
{lines}
|
||||
```
|
||||
```
|
||||
|
||||
## 💡 Example: Gitea Action Workflow
|
||||
```yaml
|
||||
name: Comment Plan on PR (Gitea)
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
plan-comment:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Tofu Plan
|
||||
id: tofu
|
||||
run: |
|
||||
tofu plan -no-color > tofu-plan.txt
|
||||
echo "plan_text<<EOF" >> $GITEA_OUTPUT
|
||||
cat tofu-plan.txt >> $GITEA_OUTPUT
|
||||
echo "EOF" >> $GITEA_OUTPUT
|
||||
|
||||
- name: Post Plan to Gitea PR
|
||||
uses: "https://gitea.example.com/your-org/pr-commenter-action@main"
|
||||
- name: Post PR Comment
|
||||
uses: your-org/git-auto-comment@main
|
||||
with:
|
||||
platform: gitea
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
repo_owner: "tar-valon"
|
||||
repo_name: "terraform-configs"
|
||||
pr_index: ${{ github.event.pull_request.number }}
|
||||
api_url: "https://gitea.example.com/api/v1"
|
||||
pr_index: ${{ gitea.event.pull_request.number }}
|
||||
content: ${{ steps.tofu.outputs.plan_text }}
|
||||
diff: |
|
||||
+ added line one
|
||||
+ added line two
|
||||
comment_template: |
|
||||
🧠 **OpenTofu Plan Diff**
|
||||
🧱 **Terraform Plan Output**
|
||||
```
|
||||
{lines}
|
||||
```
|
||||
debug: "true"
|
||||
```
|
||||
## 🧩 Debug Mode
|
||||
|
||||
## 🪵 Example: Posting Large Log Output
|
||||
```yaml
|
||||
- name: Upload build logs to PR
|
||||
uses: "https://gitea.example.com/your-org/pr-commenter-action@main"
|
||||
with:
|
||||
platform: gitea
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
api_url: "https://gitea.example.com/api/v1"
|
||||
pr_index: 42
|
||||
content: ${{ steps.build.outputs.log }}
|
||||
comment_template: |
|
||||
🧾 **Build Log Summary**
|
||||
```
|
||||
{lines}
|
||||
```
|
||||
```
|
||||
Set `debug: "true"` (or environment variable `DEBUG=true`) to enable verbose, colorized output:
|
||||
- Prints API endpoint, payload size, and preview
|
||||
- Truncates long payloads automatically
|
||||
- Highlights errors and successes in color
|
||||
|
||||
## 🧰 Local Development
|
||||
|
||||
You can test the script locally by exporting the necessary environment variables and running:
|
||||
|
||||
```
|
||||
## 🧪 Local Testing
|
||||
```bash
|
||||
export PLATFORM=gitea
|
||||
export TOKEN=<your_token>
|
||||
export REPO_OWNER=your-org
|
||||
export REPO_NAME=your-repo
|
||||
export TOKEN=your_token
|
||||
export REPO_OWNER=myorg
|
||||
export REPO_NAME=myrepo
|
||||
export PR_INDEX=42
|
||||
export API_URL=https://gitea.example.com/api/v1
|
||||
export CONTENT="$(cat plan.txt)"
|
||||
python3 comment_pr.py
|
||||
```
|
||||
export COMMENT_TEMPLATE="Plan output:\n{lines}"
|
||||
export DIFF="$(git diff HEAD~1)"
|
||||
export DEBUG=true
|
||||
|
||||
## ⚠️ Limitations & Tips for Large Files
|
||||
|
||||
1. GitHub & Gitea Comment Limits
|
||||
- GitHub: max ~65,536 characters per comment.
|
||||
- Gitea: may vary depending on server configuration.
|
||||
|
||||
2. Chunk Large Content
|
||||
- For extremely long plans/logs, split content into smaller chunks and post multiple comments.
|
||||
- Example using shell `split`:
|
||||
|
||||
```bash
|
||||
split -l 5000 plan.txt plan_chunk_
|
||||
for file in plan_chunk_*; do
|
||||
CONTENT=$(cat "$file")
|
||||
python3 comment_pr.py ...
|
||||
done
|
||||
```
|
||||
|
||||
3. Diff Parsing
|
||||
- If your content is a diff, {line} and {lines} placeholders work.
|
||||
- Otherwise, the action will post the entire content under {lines}.
|
||||
|
||||
4. Avoid Passing Huge Strings via Env Variables
|
||||
- Always prefer writing output to a file and reading it in the script.
|
||||
python git-auto-comment.py
|
||||
```
|
||||
+49
-32
@@ -1,61 +1,78 @@
|
||||
name: "PR Commenter (GitHub/Gitea Compatible)"
|
||||
description: "Posts PR comments to GitHub or Gitea, supporting diffs or large file inputs (plans, logs, etc.)."
|
||||
author: "Trez.One / AlexNorell (adapted for Gitea)"
|
||||
name: "Git Auto Comment"
|
||||
description: "Automatically post pull request comments or reviews on Gitea or GitHub."
|
||||
author: "Charish Patel"
|
||||
branding:
|
||||
icon: "message-square"
|
||||
color: "blue"
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "Target platform: github or gitea"
|
||||
description: "Platform to use (github or gitea)."
|
||||
required: false
|
||||
default: "github"
|
||||
|
||||
token:
|
||||
description: "Auth token for GitHub or Gitea API"
|
||||
description: "API token for authentication."
|
||||
required: true
|
||||
repo_name:
|
||||
description: "Repository name (defaults to GITHUB_REPOSITORY)"
|
||||
required: false
|
||||
|
||||
repo_owner:
|
||||
description: "Repository owner (defaults to GITHUB_REPOSITORY_OWNER)"
|
||||
required: false
|
||||
api_url:
|
||||
description: "API base URL (required for Gitea)"
|
||||
required: false
|
||||
pr_index:
|
||||
description: "Pull request index or number"
|
||||
description: "Repository owner."
|
||||
required: true
|
||||
plan_file:
|
||||
description: "Path to file containing large plan/log content"
|
||||
|
||||
repo_name:
|
||||
description: "Repository name."
|
||||
required: true
|
||||
|
||||
pr_index:
|
||||
description: "Pull request index or issue number."
|
||||
required: true
|
||||
|
||||
api_url:
|
||||
description: "Base API URL (required for Gitea)."
|
||||
required: false
|
||||
|
||||
diff:
|
||||
description: "Diff or plan text to include in the comment."
|
||||
required: false
|
||||
|
||||
comment_template:
|
||||
description: "Template for comment body (supports {line} and {lines})"
|
||||
description: "Template for comment body. Supports {line} and {lines} placeholders."
|
||||
required: false
|
||||
default: |
|
||||
🚀 **Automated Comment**
|
||||
---
|
||||
{lines}
|
||||
default: "Auto-comment: changed line -> {line}"
|
||||
|
||||
debug:
|
||||
description: "Enable verbose debug logging with colorized output."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Python virtual environment
|
||||
- name: Install Python 3
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y python3 python3-venv python3-pip
|
||||
|
||||
- name: Set up venv and install deps
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r $GITHUB_ACTION_PATH/requirements.txt
|
||||
echo "VENV_PATH=$PWD/venv" >> $GITHUB_ENV
|
||||
pip install -r ${{ github.action_path }}/requirements.txt
|
||||
|
||||
- name: Run git-auto-comment
|
||||
shell: bash
|
||||
run: |
|
||||
source $VENV_PATH/bin/activate
|
||||
python3 $GITHUB_ACTION_PATH/git-auto-comment.py
|
||||
env:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
TOKEN: ${{ inputs.token }}
|
||||
REPO_NAME: ${{ inputs.repo_name }}
|
||||
REPO_OWNER: ${{ inputs.repo_owner }}
|
||||
API_URL: ${{ inputs.api_url }}
|
||||
REPO_NAME: ${{ inputs.repo_name }}
|
||||
PR_INDEX: ${{ inputs.pr_index }}
|
||||
PLAN_FILE: ${{ inputs.plan_file }}
|
||||
API_URL: ${{ inputs.api_url }}
|
||||
DIFF: ${{ inputs.diff }}
|
||||
COMMENT_TEMPLATE: ${{ inputs.comment_template }}
|
||||
DEBUG: ${{ inputs.debug }}
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python ${{ github.action_path }}/git-auto-comment.py
|
||||
|
||||
+97
-65
@@ -3,83 +3,115 @@ import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
|
||||
# --- Check for required dependency ---
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print(
|
||||
"❌ The 'requests' library is not installed.\n"
|
||||
"Please ensure it's available in your runner.\n"
|
||||
"If using a composite action, add:\n"
|
||||
" - name: Install Python dependencies\n"
|
||||
" run: |\n"
|
||||
" python3 -m pip install --upgrade pip\n"
|
||||
" python3 -m pip install requests",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
# --- ANSI color helpers ---
|
||||
def color(text, code):
|
||||
return f"\033[{code}m{text}\033[0m"
|
||||
|
||||
# --- Read environment variables ---
|
||||
def green(text): return color(text, "32")
|
||||
def red(text): return color(text, "31")
|
||||
def yellow(text): return color(text, "33")
|
||||
def cyan(text): return color(text, "36")
|
||||
def bold(text): return color(text, "1")
|
||||
|
||||
# --- Inputs ---
|
||||
platform = os.environ.get("PLATFORM", "github").lower()
|
||||
token = os.environ.get("TOKEN")
|
||||
owner = os.environ.get("REPO_OWNER", os.environ.get("GITHUB_REPOSITORY_OWNER"))
|
||||
repo = os.environ.get("REPO_NAME", os.environ.get("GITHUB_REPOSITORY"))
|
||||
pr_index = os.environ.get("PR_INDEX")
|
||||
api_url = os.environ.get("API_URL", os.environ.get("GITHUB_API_URL"))
|
||||
plan_file = os.environ.get("PLAN_FILE")
|
||||
diff_text = os.environ.get("DIFF") # still supported for legacy usage
|
||||
diff_text = os.environ.get("DIFF")
|
||||
comment_template = os.environ.get("COMMENT_TEMPLATE", "Auto-comment: changed line -> {line}")
|
||||
debug_mode = os.environ.get("DEBUG", "false").lower() == "true"
|
||||
|
||||
if not token or not pr_index:
|
||||
print("❌ TOKEN and PR_INDEX are required.", file=sys.stderr)
|
||||
print(red("❌ TOKEN and PR_INDEX are required."))
|
||||
sys.exit(1)
|
||||
|
||||
# --- Load content from file or environment ---
|
||||
content_text = None
|
||||
if plan_file and os.path.exists(plan_file):
|
||||
try:
|
||||
with open(plan_file, "r", encoding="utf-8") as f:
|
||||
content_text = f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to read file '{plan_file}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif diff_text:
|
||||
content_text = diff_text
|
||||
|
||||
# --- Prepare comment body ---
|
||||
if not content_text:
|
||||
body = comment_template.replace("{line}", "").replace("{lines}", "")
|
||||
else:
|
||||
body = comment_template.replace("{lines}", content_text).replace("{line}", "")
|
||||
|
||||
# --- Determine API endpoint ---
|
||||
if platform == "github":
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_index}/comments"
|
||||
elif platform == "gitea":
|
||||
if not api_url:
|
||||
print("❌ Gitea API URL required for Gitea platform", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
url = f"{api_url}/repos/{owner}/{repo}/pulls/{pr_index}/comments"
|
||||
else:
|
||||
print(f"❌ Unsupported platform: {platform}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Post the comment ---
|
||||
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||
payload = {"body": body}
|
||||
|
||||
print(f"🛰️ Posting comment to {platform.upper()} PR #{pr_index}...")
|
||||
|
||||
try:
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
if resp.status_code in (200, 201):
|
||||
print("✅ Comment posted successfully!")
|
||||
sys.exit(0)
|
||||
# --- Build URLs and headers ---
|
||||
def build_comment_url():
|
||||
if platform == "github":
|
||||
return f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_index}/comments"
|
||||
elif platform == "gitea":
|
||||
if not api_url:
|
||||
print(red("❌ API_URL required for Gitea platform."))
|
||||
sys.exit(1)
|
||||
return f"{api_url}/repos/{owner}/{repo}/pulls/{pr_index}/reviews"
|
||||
else:
|
||||
print(f"❌ Failed to post comment: {resp.status_code}", file=sys.stderr)
|
||||
print(resp.text, file=sys.stderr)
|
||||
print(red(f"❌ Unsupported platform: {platform}"))
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Network or API error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def build_headers():
|
||||
return {
|
||||
"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
# --- POST comment ---
|
||||
def post_comment(body_text):
|
||||
url = build_comment_url()
|
||||
headers = build_headers()
|
||||
payload = {"body": body_text}
|
||||
|
||||
if debug_mode:
|
||||
preview = (body_text[:400] + "... [truncated]") if len(body_text) > 400 else body_text
|
||||
print(bold(cyan("🧩 DEBUG MODE ENABLED")))
|
||||
print(f" {yellow('Platform:')} {platform}")
|
||||
print(f" {yellow('Repo:')} {owner}/{repo}")
|
||||
print(f" {yellow('PR Index:')} {pr_index}")
|
||||
print(f" {yellow('Endpoint:')} {url}")
|
||||
print(f" {yellow('Comment size:')} {len(body_text)} characters")
|
||||
print(f" {yellow('Payload preview:')} {json.dumps(payload)[:200]}{'... [truncated]' if len(json.dumps(payload))>200 else ''}")
|
||||
print(f" {yellow('Comment preview:')}\n{preview}\n{'-'*60}")
|
||||
|
||||
try:
|
||||
resp = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
if resp.status_code in (200, 201):
|
||||
print(green(f"✅ Comment posted successfully to {platform.capitalize()}!"))
|
||||
else:
|
||||
print(red(f"❌ Failed to post comment ({resp.status_code}):"))
|
||||
print(resp.text)
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(red(f"❌ Request error: {e}"))
|
||||
sys.exit(1)
|
||||
|
||||
# --- No diff provided ---
|
||||
if not diff_text:
|
||||
body = comment_template.replace("{line}", "").replace("{lines}", "")
|
||||
post_comment(body)
|
||||
sys.exit(0)
|
||||
|
||||
# --- Parse unified diff ---
|
||||
diff_files = {}
|
||||
current_file = None
|
||||
new_line_num = 0
|
||||
|
||||
for line in diff_text.splitlines():
|
||||
if line.startswith("+++ b/"):
|
||||
current_file = line[6:].strip()
|
||||
diff_files[current_file] = []
|
||||
new_line_num = 0
|
||||
elif line.startswith("@@"):
|
||||
m = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", line)
|
||||
if m:
|
||||
new_line_num = int(m.group(1)) - 1
|
||||
elif line.startswith("+") and not line.startswith("+++"):
|
||||
new_line_num += 1
|
||||
content = line[1:]
|
||||
diff_files[current_file].append((new_line_num, content))
|
||||
elif not line.startswith("-"):
|
||||
new_line_num += 1
|
||||
|
||||
# --- Post combined comment per file ---
|
||||
for file_path, lines in diff_files.items():
|
||||
if not lines:
|
||||
continue
|
||||
all_lines_content = "\n".join([line_content for _, line_content in lines])
|
||||
body = comment_template.replace("{lines}", all_lines_content)
|
||||
post_comment(body)
|
||||
|
||||
print(green("🎉 All comments posted successfully."))
|
||||
|
||||
Reference in New Issue
Block a user