feat: add GitHub Actions support #75

Closed
rodin wants to merge 1 commits from github-support into main
5 changed files with 406 additions and 2 deletions
Showing only changes of commit dd003c66d5 - Show all commits
+200
View File
@@ -0,0 +1,200 @@
# This composite action is designed for Gitea Actions runners.
aweiker marked this conversation as resolved Outdated
Outdated
Review

[MINOR] The action comment says 'This composite action is designed for Gitea Actions runners' but this file is now in .github/ for GitHub Actions. The comment is misleading for GitHub Actions users who encounter this file. Consider updating the comment to reflect dual-platform support.

**[MINOR]** The action comment says 'This composite action is designed for Gitea Actions runners' but this file is now in `.github/` for GitHub Actions. The comment is misleading for GitHub Actions users who encounter this file. Consider updating the comment to reflect dual-platform support.
Review

[MINOR] The comment at the top says 'This composite action is designed for Gitea Actions runners' but the file is now in .github/ and intended to run on GitHub Actions as well. The comment is misleading for GitHub Actions users who discover this action.

**[MINOR]** The comment at the top says 'This composite action is designed for Gitea Actions runners' but the file is now in `.github/` and intended to run on GitHub Actions as well. The comment is misleading for GitHub Actions users who discover this action.
# Gitea Actions supports GitHub Actions syntax including $GITHUB_OUTPUT,
# actions/cache, and actions/checkout.
# Requirements: python3, sha256sum, curl (all present on ubuntu-* runners).
name: 'AI Code Review'
description: 'Run AI-powered code review on a pull request using review-bot'
inputs:
gitea-url:
description: 'Gitea instance URL (defaults to server_url)'
required: false
default: ''
repo:
description: 'Repository (owner/name, defaults to current)'
required: false
default: ''
pr-number:
description: 'Pull request number (defaults to current PR)'
required: false
default: ''
reviewer-token:
description: 'Gitea token for posting the review'
required: true
reviewer-name:
description: 'Display name for the reviewer'
required: false
default: ''
llm-base-url:
description: 'OpenAI-compatible LLM API base URL (not required for aicore provider)'
required: false
default: ''
llm-api-key:
description: 'LLM API key (not required for aicore provider)'
required: false
default: ''
llm-model:
description: 'LLM model name'
required: true
llm-provider:
description: 'LLM API provider: openai, anthropic, or aicore (default openai)'
required: false
default: 'openai'
aicore-client-id:
description: 'SAP AI Core client ID (required for aicore provider)'
required: false
default: ''
aicore-client-secret:
description: 'SAP AI Core client secret (required for aicore provider)'
required: false
default: ''
aicore-auth-url:
description: 'SAP AI Core authentication URL (required for aicore provider)'
required: false
default: ''
aicore-api-url:
description: 'SAP AI Core API URL (required for aicore provider)'
required: false
default: ''
aicore-resource-group:
description: 'SAP AI Core resource group (default: default)'
required: false
default: 'default'
conventions-file:
description: 'Path to conventions file in the repo (e.g. CLAUDE.md)'
required: false
default: ''
patterns-repo:
description: 'Comma-separated repos with language patterns (e.g. rodin/elixir-patterns,rodin/phoenix-conventions)'
aweiker marked this conversation as resolved
Review

[MINOR] Input docs say repo defaults to current, but Determine version sets REPO to "${{ inputs.repo || 'rodin/review-bot' }}". This contradicts the description and may download binaries from the wrong repo by default. Align the default and docs (prefer defaulting to the current repository).

**[MINOR]** Input docs say repo defaults to current, but Determine version sets REPO to "${{ inputs.repo || 'rodin/review-bot' }}". This contradicts the description and may download binaries from the wrong repo by default. Align the default and docs (prefer defaulting to the current repository).
required: false
default: ''
patterns-files:
description: 'Comma-separated file paths or directories to fetch from patterns repos'
required: false
default: 'README.md'
temperature:
description: 'LLM temperature (0 = server default)'
required: false
default: '0'
timeout:
description: 'LLM request timeout in seconds (default 300)'
required: false
default: '300'
version:
description: 'review-bot version to install (e.g. v0.1.0, defaults to latest)'
required: false
default: 'latest'
dry-run:
aweiker marked this conversation as resolved
Review

[NIT] The REPO default for the binary download is hardcoded to rodin/review-bot. When running on GitHub.com (mirrored as aweiker/ai-core-review-bot), the download will still attempt to fetch from the Gitea instance at github.server_url (which would be https://github.com) using the Gitea API path /api/v1/repos/rodin/review-bot/releases. This will 404. The action works for Gitea runners but will break if actually invoked as a reusable action on GitHub.com. Consider documenting that gitea-url must be explicitly set when using from GitHub.

**[NIT]** The `REPO` default for the binary download is hardcoded to `rodin/review-bot`. When running on GitHub.com (mirrored as `aweiker/ai-core-review-bot`), the download will still attempt to fetch from the Gitea instance at `github.server_url` (which would be `https://github.com`) using the Gitea API path `/api/v1/repos/rodin/review-bot/releases`. This will 404. The action works for Gitea runners but will break if actually invoked as a reusable action on GitHub.com. Consider documenting that `gitea-url` must be explicitly set when using from GitHub.
description: 'Print review to stdout instead of posting'
required: false
default: 'false'
update-existing:
Review

[MINOR] The composite action constructs download and release URLs assuming a Gitea server (e.g., /api/v1 and /releases/download paths). If this action is intended to run on GitHub, it should detect the provider and use GitHub's API for version discovery and asset downloads; otherwise document it as Gitea-only to avoid confusion.

**[MINOR]** The composite action constructs download and release URLs assuming a Gitea server (e.g., /api/v1 and /releases/download paths). If this action is intended to run on GitHub, it should detect the provider and use GitHub's API for version discovery and asset downloads; otherwise document it as Gitea-only to avoid confusion.
description: 'Delete previous review from same bot after posting new one. Accepts: true/1/yes or false/0/no (default true)'
required: false
default: 'true'
system-prompt-file:
description: 'Local file with additional system prompt instructions (e.g. security review focus)'
required: false
default: ''
persona:
description: 'Built-in persona name (security, architect, docs)'
aweiker marked this conversation as resolved
Review

[NIT] The Determine version step hardcodes REPO as 'rodin/review-bot' for the download source. On GitHub, this repo may not exist or be accessible, meaning the Install review-bot step would fail for GitHub Actions users trying to use this composite action from the public mirror. This is likely intentional (the composite action is primarily for Gitea) but deserves a comment.

**[NIT]** The `Determine version` step hardcodes `REPO` as `'rodin/review-bot'` for the download source. On GitHub, this repo may not exist or be accessible, meaning the `Install review-bot` step would fail for GitHub Actions users trying to use this composite action from the public mirror. This is likely intentional (the composite action is primarily for Gitea) but deserves a comment.
required: false
default: ''
persona-file:
description: 'Path to custom persona JSON file'
aweiker marked this conversation as resolved
Review

[MINOR] The cache key for the downloaded binary only uses the version (review-bot-linux-amd64-${{ steps.version.outputs.version }}). If inputs.gitea-url or inputs.repo differ between runs but the tag is the same, a stale or wrong binary could be reused without checksum revalidation. Consider including gitea-url and repo in the cache key or validating checksum on cache hits.

**[MINOR]** The cache key for the downloaded binary only uses the version (review-bot-linux-amd64-${{ steps.version.outputs.version }}). If inputs.gitea-url or inputs.repo differ between runs but the tag is the same, a stale or wrong binary could be reused without checksum revalidation. Consider including gitea-url and repo in the cache key or validating checksum on cache hits.
required: false
default: ''
runs:
using: 'composite'
steps:
- name: Determine version
id: version
shell: bash
run: |
aweiker marked this conversation as resolved
Review

[MAJOR] Determine version step defaults GITEA_URL to github.server_url and queries "${GITEA_URL}/api/v1/repos/${REPO}/releases" (Gitea API). On GitHub (https://github.com) this fails. Make gitea-url required when targeting Gitea releases or add logic to detect GitHub vs Gitea and call the correct API.

**[MAJOR]** Determine version step defaults GITEA_URL to github.server_url and queries "${GITEA_URL}/api/v1/repos/${REPO}/releases" (Gitea API). On GitHub (https://github.com) this fails. Make gitea-url required when targeting Gitea releases or add logic to detect GitHub vs Gitea and call the correct API.
GITEA_URL="${{ inputs.gitea-url || github.server_url }}"
REPO="${{ inputs.repo || 'rodin/review-bot' }}"
if [ "${{ inputs.version }}" = "latest" ]; then
VERSION=$(curl -sSf "${GITEA_URL}/api/v1/repos/${REPO}/releases?limit=1" \
| python3 -c "import sys, json; releases = json.load(sys.stdin); print(releases[0]['tag_name'] if releases else '')")
security-review-bot marked this conversation as resolved
Review

[MINOR] curl invocations lack explicit timeouts, which could lead to hung jobs and potential denial-of-service on runners if the endpoint stalls. Add --connect-timeout and --max-time to bound network operations.

**[MINOR]** curl invocations lack explicit timeouts, which could lead to hung jobs and potential denial-of-service on runners if the endpoint stalls. Add --connect-timeout and --max-time to bound network operations.
if [ -z "$VERSION" ]; then
echo "Failed to determine latest version" >&2
exit 1
fi
else
VERSION="${{ inputs.version }}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Cache review-bot binary
id: cache
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/review-bot
key: review-bot-linux-amd64-${{ steps.version.outputs.version }}
- name: Install review-bot
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: |
GITEA_URL="${{ inputs.gitea-url || github.server_url }}"
REPO="${{ inputs.repo || 'rodin/review-bot' }}"
VERSION="${{ steps.version.outputs.version }}"
BINARY="review-bot-linux-amd64"
curl -sSfL "${GITEA_URL}/${REPO}/releases/download/${VERSION}/${BINARY}" \
-o "${{ runner.temp }}/review-bot"
curl -sSfL "${GITEA_URL}/${REPO}/releases/download/${VERSION}/checksums.txt" \
security-review-bot marked this conversation as resolved
Review

[MINOR] The action downloads and executes a binary based on user-provided inputs (gitea-url and repo), and verifies integrity using checksums fetched from the same source. If an untrusted workflow configuration can change these inputs, this allows executing arbitrary code with workflow secrets. Pin the source repository/host, or verify signatures with a trusted key, and avoid allowing untrusted overrides for repo/host.

**[MINOR]** The action downloads and executes a binary based on user-provided inputs (gitea-url and repo), and verifies integrity using checksums fetched from the same source. If an untrusted workflow configuration can change these inputs, this allows executing arbitrary code with workflow secrets. Pin the source repository/host, or verify signatures with a trusted key, and avoid allowing untrusted overrides for repo/host.
-o "${{ runner.temp }}/checksums.txt"
security-review-bot marked this conversation as resolved
Review

[MINOR] Additional curl downloads of the binary and checksums also lack explicit timeouts. Apply connection and overall timeouts to these requests to prevent job hangs.

**[MINOR]** Additional curl downloads of the binary and checksums also lack explicit timeouts. Apply connection and overall timeouts to these requests to prevent job hangs.
# Verify SHA-256 checksum
cd "${{ runner.temp }}"
EXPECTED=$(grep "${BINARY}" checksums.txt | awk '{print $1}')
ACTUAL=$(sha256sum review-bot | awk '{print $1}')
if [ -z "$EXPECTED" ]; then
echo "Error: no checksum found for ${BINARY}" >&2
exit 1
fi
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "Error: checksum mismatch!" >&2
echo " Expected: $EXPECTED" >&2
echo " Actual: $ACTUAL" >&2
exit 1
Review

[MINOR] The GITHUB_REPOSITORY env var is set to ${{ inputs.repo || github.repository }}. When running from GitHub Actions on the mirror repo, github.repository would be aweiker/ai-core-review-bot (or similar), not rodin/review-bot. This means if inputs.repo is not explicitly provided, the review-bot would try to post to the wrong Gitea repository. The composite action's repo input has no default (default: ''), which means the fallback to github.repository is always in play when callers don't pass repo. This could silently produce incorrect behavior rather than a clear error. Consider either making repo required or defaulting it to rodin/review-bot to match the documented intent.

**[MINOR]** The `GITHUB_REPOSITORY` env var is set to `${{ inputs.repo || github.repository }}`. When running from GitHub Actions on the mirror repo, `github.repository` would be `aweiker/ai-core-review-bot` (or similar), not `rodin/review-bot`. This means if `inputs.repo` is not explicitly provided, the review-bot would try to post to the wrong Gitea repository. The composite action's `repo` input has no default (`default: ''`), which means the fallback to `github.repository` is always in play when callers don't pass `repo`. This could silently produce incorrect behavior rather than a clear error. Consider either making `repo` required or defaulting it to `rodin/review-bot` to match the documented intent.
fi
chmod +x "${{ runner.temp }}/review-bot"
echo "Installed review-bot ${VERSION} (checksum verified)"
- name: Run review
Review

[MINOR] The environment variable UPDATE_EXISTING is set from the action input, but the main program (cmd/review-bot/main.go) does not currently read or honor this flag. Either wire this through as a flag/env in the application or remove the input to avoid confusion.

**[MINOR]** The environment variable UPDATE_EXISTING is set from the action input, but the main program (cmd/review-bot/main.go) does not currently read or honor this flag. Either wire this through as a flag/env in the application or remove the input to avoid confusion.
shell: bash
env:
GITHUB_SERVER_URL: ${{ inputs.gitea-url || github.server_url }}
GITHUB_REPOSITORY: ${{ inputs.repo || github.repository }}
PR_NUMBER: ${{ inputs.pr-number || github.event.pull_request.number }}
REVIEWER_TOKEN: ${{ inputs.reviewer-token }}
REVIEWER_NAME: ${{ inputs.reviewer-name }}
LLM_BASE_URL: ${{ inputs.llm-base-url }}
LLM_API_KEY: ${{ inputs.llm-api-key }}
LLM_MODEL: ${{ inputs.llm-model }}
CONVENTIONS_FILE: ${{ inputs.conventions-file }}
PATTERNS_REPO: ${{ inputs.patterns-repo }}
PATTERNS_FILES: ${{ inputs.patterns-files }}
LLM_TEMPERATURE: ${{ inputs.temperature }}
LLM_TIMEOUT: ${{ inputs.timeout }}
LLM_PROVIDER: ${{ inputs.llm-provider }}
UPDATE_EXISTING: ${{ inputs.update-existing }}
SYSTEM_PROMPT_FILE: ${{ inputs.system-prompt-file }}
PERSONA: ${{ inputs.persona }}
PERSONA_FILE: ${{ inputs.persona-file }}
AICORE_CLIENT_ID: ${{ inputs.aicore-client-id }}
AICORE_CLIENT_SECRET: ${{ inputs.aicore-client-secret }}
AICORE_AUTH_URL: ${{ inputs.aicore-auth-url }}
AICORE_API_URL: ${{ inputs.aicore-api-url }}
AICORE_RESOURCE_GROUP: ${{ inputs.aicore-resource-group }}
run: |
ARGS=""
if [ "${{ inputs.dry-run }}" = "true" ]; then
ARGS="--dry-run"
fi
${{ runner.temp }}/review-bot $ARGS
+69
View File
@@ -0,0 +1,69 @@
name: CI
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-24.04
steps:
Review

[NIT] Go version 1.26 is specified. As of early 2025, Go 1.26 is not yet released (latest stable is 1.23/1.24). If actions/setup-go resolves this to a future version that doesn't exist yet, the job would fail. This likely works today if setup-go tolerates forward-looking versions, but it's worth confirming and possibly pinning to a released version. (The Gitea CI presumably already uses this same version and passes, so this may be a non-issue in practice.)

**[NIT]** Go version `1.26` is specified. As of early 2025, Go 1.26 is not yet released (latest stable is 1.23/1.24). If `actions/setup-go` resolves this to a future version that doesn't exist yet, the job would fail. This likely works today if setup-go tolerates forward-looking versions, but it's worth confirming and possibly pinning to a released version. (The Gitea CI presumably already uses this same version and passes, so this may be a non-issue in practice.)
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
Review

[NIT] Go version is specified as 1.26, which does not exist yet (latest stable is 1.24.x as of mid-2025). This may cause actions/setup-go to fail or resolve to an unexpected version on GitHub Actions. Recommend pinning to a known stable version like 1.24.

**[NIT]** Go version is specified as `1.26`, which does not exist yet (latest stable is 1.24.x as of mid-2025). This may cause `actions/setup-go` to fail or resolve to an unexpected version on GitHub Actions. Recommend pinning to a known stable version like `1.24`.
Review

[NIT] Go version is set to 1.26 which does not yet exist (current latest stable is 1.24.x). This may cause actions/setup-go to fail on github.com if it cannot resolve the version, whereas Gitea's runner may be more lenient. Consider pinning to an actual released version or using stable.

**[NIT]** Go version is set to `1.26` which does not yet exist (current latest stable is 1.24.x). This may cause `actions/setup-go` to fail on github.com if it cannot resolve the version, whereas Gitea's runner may be more lenient. Consider pinning to an actual released version or using `stable`.
with:
go-version: '1.26'
- run: go test ./...
- run: go vet ./...
- run: go build -o review-bot ./cmd/review-bot
# Self-review using native SAP AI Core provider
# Models must match SAP AI Core deployments
# Available models: gpt-5, anthropic--claude-4.6-sonnet, anthropic--claude-4.6-opus
# Removed gpt-4.1, gpt-5-mini, gpt-4.1-mini - not deployed on AI Core
review:
runs-on: ubuntu-24.04
if: github.event_name == 'pull_request'
needs: test
strategy:
matrix:
include:
- name: sonnet
token_secret: SONNET_REVIEW_TOKEN
model: anthropic--claude-4.6-sonnet
- name: gpt
token_secret: GPT_REVIEW_TOKEN
model: gpt-5
- name: security
token_secret: SECURITY_REVIEW_TOKEN
model: gpt-5
patterns_repo: rodin/security-patterns
patterns_files: "."
system_prompt_file: SECURITY_REVIEW.md
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- run: go build -o review-bot ./cmd/review-bot
Review

[MAJOR] The review job sets GITHUB_SERVER_URL and GITHUB_REPOSITORY and runs ./review-bot, which will use those to target a Gitea client. On GitHub PRs this points to github.com and will not match Gitea APIs, leading to failures when fetching PRs/diffs and posting reviews. Pass a real Gitea URL if the bot should review on Gitea, or implement a GitHub path.

**[MAJOR]** The review job sets GITHUB_SERVER_URL and GITHUB_REPOSITORY and runs ./review-bot, which will use those to target a Gitea client. On GitHub PRs this points to github.com and will not match Gitea APIs, leading to failures when fetching PRs/diffs and posting reviews. Pass a real Gitea URL if the bot should review on Gitea, or implement a GitHub path.
- name: Run ${{ matrix.name }} review
env:
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
Review

[MAJOR] The review job sets GITHUB_SERVER_URL to github.server_url and runs ./review-bot, which initializes a Gitea client using this URL. GitHub’s REST API is not Gitea-compatible, so PR metadata/diff retrieval will fail on GitHub. Either set GITEA_URL to your Gitea instance (if targeting Gitea) or implement/use a GitHub client path.

**[MAJOR]** The review job sets GITHUB_SERVER_URL to github.server_url and runs ./review-bot, which initializes a Gitea client using this URL. GitHub’s REST API is not Gitea-compatible, so PR metadata/diff retrieval will fail on GitHub. Either set GITEA_URL to your Gitea instance (if targeting Gitea) or implement/use a GitHub client path.
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEWER_TOKEN: ${{ secrets[matrix.token_secret] }}
security-review-bot marked this conversation as resolved
Review

[MAJOR] Secrets are provided to a pull_request job that builds and executes code from the PR (e.g., REVIEWER_TOKEN and multiple AICORE_* secrets). A malicious PR could modify the code to exfiltrate these secrets. Best practice is to avoid using secrets in workflows that run untrusted PR code.

**[MAJOR]** Secrets are provided to a pull_request job that builds and executes code from the PR (e.g., REVIEWER_TOKEN and multiple AICORE_* secrets). A malicious PR could modify the code to exfiltrate these secrets. Best practice is to avoid using secrets in workflows that run untrusted PR code.
REVIEWER_NAME: ${{ matrix.name }}
LLM_PROVIDER: aicore
LLM_MODEL: ${{ matrix.model }}
AICORE_CLIENT_ID: ${{ secrets.AICORE_CLIENT_ID }}
AICORE_CLIENT_SECRET: ${{ secrets.AICORE_CLIENT_SECRET }}
AICORE_AUTH_URL: ${{ secrets.AICORE_AUTH_URL }}
AICORE_API_URL: ${{ secrets.AICORE_API_URL }}
AICORE_RESOURCE_GROUP: ${{ secrets.AICORE_RESOURCE_GROUP }}
CONVENTIONS_FILE: "CONVENTIONS.md"
PATTERNS_REPO: ${{ matrix.patterns_repo || 'rodin/go-patterns' }}
PATTERNS_FILES: ${{ matrix.patterns_files || 'README.md,patterns/' }}
LLM_TIMEOUT: "600"
SYSTEM_PROMPT_FILE: ${{ matrix.system_prompt_file }}
run: ./review-bot
+38
View File
@@ -0,0 +1,38 @@
name: PR Ready Gate
on:
pull_request:
types: [synchronize]
jobs:
clear-labels:
runs-on: ubuntu-24.04
# Always run - curl commands are safe if labels don't exist
steps:
- name: Remove ready and self-reviewed labels, reassign to author
env:
GITEA_TOKEN: ${{ secrets.RODIN_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
AUTHOR=${{ github.event.pull_request.user.login }}
READY_LABEL_ID=38
SELF_REVIEWED_LABEL_ID=37
Review

[MINOR] Workflow targets Gitea API using GitHub PR number and repository. PR numbers generally won’t match across GitHub and Gitea, so operations will likely be no-ops. Consider gating this workflow to only run where the PR exists (or map PR numbers), or explicitly document that it manipulates the Gitea mirror.

**[MINOR]** Workflow targets Gitea API using GitHub PR number and repository. PR numbers generally won’t match across GitHub and Gitea, so operations will likely be no-ops. Consider gating this workflow to only run where the PR exists (or map PR numbers), or explicitly document that it manipulates the Gitea mirror.
# Remove ready label if present
curl -sS -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
Review

[MINOR] The Gitea API URL is hardcoded as https://gitea.weiker.me rather than using github.server_url. This workflow will fail silently (curl will hit the Gitea instance) when running on GitHub.com, since secrets.RODIN_TOKEN won't be a valid GitHub token and the API path (/api/v1/) is Gitea-specific. If this workflow is intended to be a no-op on GitHub, that should be explicitly documented or the job should be conditioned on the server URL.

**[MINOR]** The Gitea API URL is hardcoded as `https://gitea.weiker.me` rather than using `github.server_url`. This workflow will fail silently (curl will hit the Gitea instance) when running on GitHub.com, since `secrets.RODIN_TOKEN` won't be a valid GitHub token and the API path (`/api/v1/`) is Gitea-specific. If this workflow is intended to be a no-op on GitHub, that should be explicitly documented or the job should be conditioned on the server URL.
Review

[MINOR] Label IDs 38 and 37 are hardcoded without explanation. If the Gitea instance label IDs ever change (e.g., after a data migration or re-creation), this silently does nothing. The comment explains WHY we use Gitea but not WHY these specific numeric IDs are correct. A brief comment naming which labels these IDs correspond to (e.g., # 38 = ready, 37 = self-reviewed) would improve maintainability.

**[MINOR]** Label IDs 38 and 37 are hardcoded without explanation. If the Gitea instance label IDs ever change (e.g., after a data migration or re-creation), this silently does nothing. The comment explains WHY we use Gitea but not WHY these specific numeric IDs are correct. A brief comment naming which labels these IDs correspond to (e.g., `# 38 = ready, 37 = self-reviewed`) would improve maintainability.
Review

[MINOR] This job calls a hardcoded Gitea instance (https://gitea.weiker.me) using GitHub event data. On GitHub PRs, the PR number and repo may not exist on that instance, which could fail the job. Consider scoping this job to your Gitea environment or guarding it with conditions so it doesn't run (or fail) on GitHub.

**[MINOR]** This job calls a hardcoded Gitea instance (https://gitea.weiker.me) using GitHub event data. On GitHub PRs, the PR number and repo may not exist on that instance, which could fail the job. Consider scoping this job to your Gitea environment or guarding it with conditions so it doesn't run (or fail) on GitHub.
"https://gitea.weiker.me/api/v1/repos/${{ github.repository }}/issues/${PR_NUMBER}/labels/${READY_LABEL_ID}" || true
Review

[MINOR] The label IDs (READY_LABEL_ID=38, SELF_REVIEWED_LABEL_ID=37) and the hardcoded Gitea URL (https://gitea.weiker.me) are Gitea-specific and will silently no-op (via || true) on GitHub Actions. This is acceptable for now since the workflow is a Gitea-only feature, but it means the workflow is essentially a dead stub on GitHub. A comment explaining this would reduce confusion.

**[MINOR]** The label IDs (READY_LABEL_ID=38, SELF_REVIEWED_LABEL_ID=37) and the hardcoded Gitea URL (`https://gitea.weiker.me`) are Gitea-specific and will silently no-op (via `|| true`) on GitHub Actions. This is acceptable for now since the workflow is a Gitea-only feature, but it means the workflow is essentially a dead stub on GitHub. A comment explaining this would reduce confusion.
# Remove self-reviewed label if present
curl -sS -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"https://gitea.weiker.me/api/v1/repos/${{ github.repository }}/issues/${PR_NUMBER}/labels/${SELF_REVIEWED_LABEL_ID}" || true
# Reassign to author
curl -sS -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"assignees\": [\"${AUTHOR}\"]}" \
"https://gitea.weiker.me/api/v1/repos/${{ github.repository }}/pulls/${PR_NUMBER}"
echo "Cleared ready/self-reviewed labels and reassigned PR #${PR_NUMBER} to ${AUTHOR}"
+97
View File
@@ -0,0 +1,97 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Run tests
run: |
go vet ./...
go test ./...
- name: Build binaries
run: |
VERSION=${GITHUB_REF_NAME}
mkdir -p dist
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=${VERSION}" -o dist/review-bot-linux-amd64 ./cmd/review-bot
GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X main.version=${VERSION}" -o dist/review-bot-linux-arm64 ./cmd/review-bot
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.version=${VERSION}" -o dist/review-bot-darwin-amd64 ./cmd/review-bot
GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w -X main.version=${VERSION}" -o dist/review-bot-darwin-arm64 ./cmd/review-bot
cd dist && sha256sum * > checksums.txt
- name: Create release and upload assets
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION=${GITHUB_REF_NAME}
GITEA_URL="${{ github.server_url }}"
REPO="${{ github.repository }}"
# Create release (or find existing one for this tag)
HTTP_CODE=$(curl -s -o /tmp/release_response.json -w "%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
security-review-bot marked this conversation as resolved
Review

[MINOR] Multiple curl API calls (creating/fetching releases and listing/uploading/deleting assets) do not set explicit timeouts, increasing risk of runner hangs. Add --connect-timeout and --max-time to these requests (and subsequent ones at lines ~53, ~79, ~86, ~91) to mitigate DoS via stalled endpoints.

**[MINOR]** Multiple curl API calls (creating/fetching releases and listing/uploading/deleting assets) do not set explicit timeouts, increasing risk of runner hangs. Add --connect-timeout and --max-time to these requests (and subsequent ones at lines ~53, ~79, ~86, ~91) to mitigate DoS via stalled endpoints.
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" \
-d "{\"tag_name\": \"${VERSION}\", \"name\": \"${VERSION}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}")
Review

[MAJOR] Release step uses GITEA_URL="${{ github.server_url }}" and then calls "${GITEA_URL}/api/v1/repos/.../releases". On GitHub, server_url is https://github.com (GitHub releases live at api.github.com and use different endpoints). This will not work on GitHub. Use GitHub's REST API (or an action like softprops/action-gh-release) when running on GitHub, and keep the Gitea API for Gitea.

**[MAJOR]** Release step uses GITEA_URL="${{ github.server_url }}" and then calls "${GITEA_URL}/api/v1/repos/.../releases". On GitHub, server_url is https://github.com (GitHub releases live at api.github.com and use different endpoints). This will not work on GitHub. Use GitHub's REST API (or an action like softprops/action-gh-release) when running on GitHub, and keep the Gitea API for Gitea.
if [ "$HTTP_CODE" = "409" ]; then
echo "Release for ${VERSION} already exists, fetching existing..."
curl -sSf -o /tmp/release_response.json \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${VERSION}"
aweiker marked this conversation as resolved
Review

[MAJOR] Release step uses GITEA_URL="${{ github.server_url }}" and calls /api/v1 (Gitea API). On GitHub this endpoint is invalid; release creation and asset upload will fail. If releases are intended on GitHub, switch to GitHub’s Releases API (api.github.com/repos/:owner/:repo/releases) or use actions like actions/create-release. If releases are intended on Gitea, set GITEA_URL explicitly to the Gitea server.

**[MAJOR]** Release step uses GITEA_URL="${{ github.server_url }}" and calls /api/v1 (Gitea API). On GitHub this endpoint is invalid; release creation and asset upload will fail. If releases are intended on GitHub, switch to GitHub’s Releases API (api.github.com/repos/:owner/:repo/releases) or use actions like actions/create-release. If releases are intended on Gitea, set GITEA_URL explicitly to the Gitea server.
elif [ "$HTTP_CODE" != "201" ]; then
echo "Failed to create release (HTTP ${HTTP_CODE})" >&2
cat /tmp/release_response.json >&2
exit 1
fi
# Parse release ID (python3 available on ubuntu-24.04 runners)
RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release_response.json'))['id'])")
if [ -z "$RELEASE_ID" ]; then
echo "Failed to parse release ID" >&2
cat /tmp/release_response.json >&2
exit 1
fi
echo "Release ID: ${RELEASE_ID}"
# Upload each asset (idempotent: delete existing asset with same name first)
for file in dist/*; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
# Check if asset already exists and delete it
EXISTING_ID=$(export ASSET_NAME="${filename}"; curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
| python3 -c "import json,sys,os; name=os.environ['ASSET_NAME']; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']==name),''))" 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
aweiker marked this conversation as resolved
Review

[NIT] The upload script uses jq for URL-encoding without ensuring it’s present. ubuntu-24.04 runners typically include jq, but adding an explicit install or a pure-shell URL-encode avoids environment dependence.

**[NIT]** The upload script uses jq for URL-encoding without ensuring it’s present. ubuntu-24.04 runners typically include jq, but adding an explicit install or a pure-shell URL-encode avoids environment dependence.
echo " Asset ${filename} already exists (id=${EXISTING_ID}), deleting..."
curl -sSf -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
aweiker marked this conversation as resolved
Review

[MINOR] The release job uses jq to URL-encode asset names but doesn't ensure jq is installed. While Ubuntu runners often include jq, it's safer to either install it explicitly or replace this with a Python-based encoding to avoid environment drift.

**[MINOR]** The release job uses `jq` to URL-encode asset names but doesn't ensure `jq` is installed. While Ubuntu runners often include jq, it's safer to either install it explicitly or replace this with a Python-based encoding to avoid environment drift.
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${EXISTING_ID}"
fi
curl -sSf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=$(printf '%s' "${filename}" | jq -sRr @uri)" \
--data-binary "@${file}"
done
echo "Release ${VERSION} created with assets"
+2 -2
View File
@@ -54,8 +54,8 @@ func main() {
logFormat := flag.String("log-format", envOrDefault("LOG_FORMAT", "text"), "Log output format: text or json")
verbosity := flag.String("verbosity", envOrDefault("LOG_VERBOSITY", "info"), "Log verbosity: debug, info, warn, error")
aweiker marked this conversation as resolved
Review

[NIT] The fallback to GITHUB_SERVER_URL/GITHUB_REPOSITORY is fine, but note that the client remains Gitea-specific. Consider clarifying in help text that GitHub support requires pointing to a Gitea-compatible API or implementing a GitHub client path.

**[NIT]** The fallback to GITHUB_SERVER_URL/GITHUB_REPOSITORY is fine, but note that the client remains Gitea-specific. Consider clarifying in help text that GitHub support requires pointing to a Gitea-compatible API or implementing a GitHub client path.
// CLI flags
Review

[MAJOR] Defaults now fall back to GITHUB_SERVER_URL/GITHUB_REPOSITORY for --gitea-url/--repo, but the bot constructs a Gitea client and calls Gitea v1 APIs. On GitHub, GITHUB_SERVER_URL is https://github.com and lacks /api/v1; this will cause API calls to fail. Either keep requiring explicit GITEA_URL/GITEA_REPO when targeting Gitea, or add a GitHub client and switch on provider.

**[MAJOR]** Defaults now fall back to GITHUB_SERVER_URL/GITHUB_REPOSITORY for --gitea-url/--repo, but the bot constructs a Gitea client and calls Gitea v1 APIs. On GitHub, GITHUB_SERVER_URL is https://github.com and lacks /api/v1; this will cause API calls to fail. Either keep requiring explicit GITEA_URL/GITEA_REPO when targeting Gitea, or add a GitHub client and switch on provider.
giteaURL := flag.String("gitea-url", envOrDefault("GITEA_URL", ""), "Gitea instance URL")
repo := flag.String("repo", envOrDefault("GITEA_REPO", ""), "Repository (owner/name)")
giteaURL := flag.String("gitea-url", envOrDefault("GITEA_URL", envOrDefault("GITHUB_SERVER_URL", "")), "Gitea instance URL")
repo := flag.String("repo", envOrDefault("GITEA_REPO", envOrDefault("GITHUB_REPOSITORY", "")), "Repository (owner/name)")
prNum := flag.String("pr", envOrDefault("PR_NUMBER", ""), "Pull request number")
reviewerName := flag.String("reviewer-name", envOrDefault("REVIEWER_NAME", ""), "Reviewer display name")
reviewerToken := flag.String("reviewer-token", envOrDefault("REVIEWER_TOKEN", ""), "Gitea token for posting review")