diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b34b892..944bd02 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -38,17 +38,27 @@ jobs: GITEA_URL="${{ github.server_url }}" REPO="${{ github.repository }}" - # Create release - RELEASE_ID=$(curl -sSf -X POST \ + # Create release (parse ID without jq using grep/sed) + RESPONSE=$(curl -sSf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -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}" \ - | jq -r '.id') + -d "{\"tag_name\": \"${VERSION}\", \"name\": \"${VERSION}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}") + + RELEASE_ID=$(echo "$RESPONSE" | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1) + + if [ -z "$RELEASE_ID" ]; then + echo "Failed to create release" >&2 + echo "$RESPONSE" >&2 + exit 1 + fi + + echo "Created release ID: ${RELEASE_ID}" # Upload each asset for file in dist/*; do filename=$(basename "$file") + echo "Uploading ${filename}..." curl -sSf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/octet-stream" \ @@ -56,4 +66,4 @@ jobs: --data-binary "@${file}" done - echo "Release ${VERSION} created with $(ls dist/* | wc -l) assets" + echo "Release ${VERSION} created with assets" diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index a2b26ef..e75c3ac 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -28,6 +28,7 @@ func main() { llmModel := flag.String("llm-model", envOrDefault("LLM_MODEL", ""), "LLM model name") conventionsFile := flag.String("conventions-file", envOrDefault("CONVENTIONS_FILE", ""), "Conventions file path in repo (e.g. CLAUDE.md)") dryRun := flag.Bool("dry-run", false, "Print review to stdout instead of posting") +llmTemp := flag.Float64("llm-temperature", envOrDefaultFloat("LLM_TEMPERATURE", 0), "LLM temperature (0 = server default)") flag.Parse() @@ -55,6 +56,9 @@ func main() { // Initialize clients giteaClient := gitea.NewClient(*giteaURL, *reviewerToken) llmClient := llm.NewClient(*llmBaseURL, *llmAPIKey, *llmModel) + if *llmTemp > 0 { + llmClient.WithTemperature(*llmTemp) + } log.Printf("Reviewing PR #%d on %s/%s", prNumber, owner, repoName) @@ -169,3 +173,13 @@ func envOrDefault(key, defaultVal string) string { } return defaultVal } + +func envOrDefaultFloat(key string, defaultVal float64) float64 { + if v := os.Getenv(key); v != "" { + f, err := strconv.ParseFloat(v, 64) + if err == nil { + return f + } + } + return defaultVal +} diff --git a/install.sh b/install.sh index 8ee18ad..20e9bdc 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,18 @@ set -euo pipefail GITEA_URL="${GITEA_URL:-https://gitea.weiker.me}" REPO="rodin/review-bot" -INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +INSTALL_DIR="${INSTALL_DIR:-}" + +# Determine install directory with fallback +if [ -z "$INSTALL_DIR" ]; then + if [ -w /usr/local/bin ]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + mkdir -p "$INSTALL_DIR" + echo "Note: Installing to $INSTALL_DIR (add to PATH if needed)" + fi +fi OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) @@ -18,8 +29,8 @@ esac BINARY="review-bot-${OS}-${ARCH}" -# Get latest release tag -LATEST=$(curl -sSf "${GITEA_URL}/api/v1/repos/${REPO}/releases?limit=1" | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4) +# Get latest release tag (POSIX-safe parsing without jq) +LATEST=$(curl -sSf "${GITEA_URL}/api/v1/repos/${REPO}/releases?limit=1" | sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' | head -1) if [ -z "$LATEST" ]; then echo "Failed to determine latest release" >&2 @@ -29,7 +40,36 @@ fi echo "Installing review-bot ${LATEST} (${OS}/${ARCH})..." DOWNLOAD_URL="${GITEA_URL}/${REPO}/releases/download/${LATEST}/${BINARY}" -curl -sSfL -o "${INSTALL_DIR}/review-bot" "$DOWNLOAD_URL" +CHECKSUM_URL="${GITEA_URL}/${REPO}/releases/download/${LATEST}/checksums.txt" + +# Download binary and checksums +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +curl -sSfL -o "${TMPDIR}/${BINARY}" "$DOWNLOAD_URL" +curl -sSfL -o "${TMPDIR}/checksums.txt" "$CHECKSUM_URL" + +# Verify checksum +cd "$TMPDIR" +EXPECTED=$(grep "${BINARY}" checksums.txt | awk '{print $1}') +ACTUAL=$(sha256sum "${BINARY}" | awk '{print $1}') + +if [ -z "$EXPECTED" ]; then + echo "Error: no checksum found for ${BINARY} in checksums.txt" >&2 + exit 1 +fi + +if [ "$EXPECTED" != "$ACTUAL" ]; then + echo "Error: checksum mismatch!" >&2 + echo " Expected: $EXPECTED" >&2 + echo " Actual: $ACTUAL" >&2 + exit 1 +fi + +echo "Checksum verified ✓" + +# Install +cp "${TMPDIR}/${BINARY}" "${INSTALL_DIR}/review-bot" chmod +x "${INSTALL_DIR}/review-bot" echo "Installed review-bot ${LATEST} to ${INSTALL_DIR}/review-bot" diff --git a/llm/client.go b/llm/client.go index a14a9bf..a6bbccb 100644 --- a/llm/client.go +++ b/llm/client.go @@ -14,6 +14,7 @@ type Client struct { BaseURL string APIKey string Model string + Temperature float64 HTTP *http.Client } @@ -27,6 +28,13 @@ func NewClient(baseURL, apiKey, model string) *Client { } } +// WithTemperature sets the temperature for LLM requests. +// If not set (zero value), the server default is used. +func (c *Client) WithTemperature(t float64) *Client { + c.Temperature = t + return c +} + // Message represents a chat message. type Message struct { Role string `json:"role"` @@ -53,6 +61,7 @@ type ChatResponse struct { func (c *Client) Complete(messages []Message) (string, error) { reqBody := ChatRequest{ Model: c.Model, + Temperature: c.Temperature, Messages: messages, }