diff --git a/.gitea/actions/review/action.yml b/.gitea/actions/review/action.yml index d0d9e3b..f218a02 100644 --- a/.gitea/actions/review/action.yml +++ b/.gitea/actions/review/action.yml @@ -58,8 +58,10 @@ runs: shell: bash run: | 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/rodin/review-bot/releases?limit=1" | sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' | head -1) + 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 '')") if [ -z "$VERSION" ]; then echo "Failed to determine latest version" >&2 exit 1 @@ -81,9 +83,33 @@ runs: shell: bash run: | GITEA_URL="${{ inputs.gitea-url || github.server_url }}" + REPO="${{ inputs.repo || 'rodin/review-bot' }}" VERSION="${{ steps.version.outputs.version }}" - curl -sSfL "${GITEA_URL}/rodin/review-bot/releases/download/${VERSION}/review-bot-linux-amd64" -o ${{ runner.temp }}/review-bot - chmod +x ${{ runner.temp }}/review-bot + 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" \ + -o "${{ runner.temp }}/checksums.txt" + + # 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 + fi + + chmod +x "${{ runner.temp }}/review-bot" + echo "Installed review-bot ${VERSION} (checksum verified)" - name: Run review shell: bash diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9a320ec..b268f29 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -38,23 +38,34 @@ jobs: GITEA_URL="${{ github.server_url }}" REPO="${{ github.repository }}" - # Create release - RESPONSE=$(curl -sSf -X POST \ + # 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}" \ -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}") - # Parse release ID using Python (robust JSON parsing) - RELEASE_ID=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])") - - if [ -z "$RELEASE_ID" ]; then - echo "Failed to create release" >&2 - echo "$RESPONSE" >&2 + 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}" + elif [ "$HTTP_CODE" != "201" ]; then + echo "Failed to create release (HTTP ${HTTP_CODE})" >&2 + cat /tmp/release_response.json >&2 exit 1 fi - echo "Created release ID: ${RELEASE_ID}" + # 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 for file in dist/*; do diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index 62e0e2d..48b6742 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -28,7 +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)") + llmTemp := flag.Float64("llm-temperature", envOrDefaultFloat("LLM_TEMPERATURE", 0), "LLM temperature (0 = server default)") flag.Parse() diff --git a/llm/client.go b/llm/client.go index 5af281b..1f3e580 100644 --- a/llm/client.go +++ b/llm/client.go @@ -28,10 +28,7 @@ func NewClient(baseURL, apiKey, model string) *Client { } } -// WithTemperature sets the temperature for LLM requests. -// A value of 0 (the zero value) means the field is omitted from the request, -// causing the server to use its default temperature. -// If not set (zero value), the server default is used. +// WithTemperature sets the temperature for LLM requests (0 = omit, uses server default). func (c *Client) WithTemperature(t float64) *Client { c.Temperature = t return c diff --git a/llm/client_test.go b/llm/client_test.go index 278668d..2487e4a 100644 --- a/llm/client_test.go +++ b/llm/client_test.go @@ -108,3 +108,80 @@ func TestComplete_ServerDown(t *testing.T) { t.Fatal("expected error for connection refused, got nil") } } + +func TestWithTemperature(t *testing.T) { + client := NewClient("http://example.com", "key", "model") + if client.Temperature != 0 { + t.Errorf("expected initial temperature 0, got %f", client.Temperature) + } + + result := client.WithTemperature(0.7) + if result != client { + t.Error("WithTemperature should return the same client for chaining") + } + if client.Temperature != 0.7 { + t.Errorf("expected temperature 0.7, got %f", client.Temperature) + } +} + +func TestComplete_TemperatureOmittedWhenZero(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req map[string]interface{} + json.NewDecoder(r.Body).Decode(&req) + + if _, exists := req["temperature"]; exists { + t.Error("temperature should be omitted when zero (server default)") + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ChatResponse{ + Choices: []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + }{{Message: struct { + Content string `json:"content"` + }{Content: "ok"}}}, + }) + })) + defer server.Close() + + client := NewClient(server.URL, "key", "model") + _, err := client.Complete([]Message{{Role: "user", Content: "Hi"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestComplete_TemperatureIncludedWhenSet(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req map[string]interface{} + json.NewDecoder(r.Body).Decode(&req) + + temp, exists := req["temperature"] + if !exists { + t.Error("temperature should be included when set") + } + if temp != 0.7 { + t.Errorf("expected temperature 0.7, got %v", temp) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ChatResponse{ + Choices: []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + }{{Message: struct { + Content string `json:"content"` + }{Content: "ok"}}}, + }) + })) + defer server.Close() + + client := NewClient(server.URL, "key", "model").WithTemperature(0.7) + _, err := client.Complete([]Message{{Role: "user", Content: "Hi"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +}