236 lines
6.5 KiB
Go
236 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type TimesheetPayload struct {
|
|
Date string `json:"date"`
|
|
Description string `json:"description"`
|
|
GiteaUsername string `json:"gitea_username"`
|
|
HourSpent float64 `json:"hour_spent"`
|
|
TaskID int64 `json:"task_id"`
|
|
}
|
|
|
|
// parseOdooTicketID extracts the numeric ticket id from a ref formatted like
|
|
// "<prefix>/ticket-<number>". It scans path segments and picks the first segment
|
|
// that starts with "ticket-" (case-insensitive), then reads leading digits.
|
|
func parseOdooTicketID(ref string) (int64, error) {
|
|
if strings.TrimSpace(ref) == "" {
|
|
return 0, fmt.Errorf("empty ref")
|
|
}
|
|
parts := strings.Split(ref, "/")
|
|
for _, seg := range parts {
|
|
if seg == "" {
|
|
continue
|
|
}
|
|
lower := strings.ToLower(seg)
|
|
const pfx = "ticket-"
|
|
if strings.HasPrefix(lower, pfx) {
|
|
numPart := seg[len(pfx):]
|
|
// collect leading digits
|
|
var digits strings.Builder
|
|
for _, r := range numPart {
|
|
if r >= '0' && r <= '9' {
|
|
digits.WriteRune(r)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if digits.Len() == 0 {
|
|
return 0, fmt.Errorf("no digits after ticket- in segment: %s", seg)
|
|
}
|
|
id, err := strconv.ParseInt(digits.String(), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return id, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("no ticket-<number> segment found in ref: %s", ref)
|
|
}
|
|
|
|
// readEventTrackedTime tries to extract tracked_time.time and tracked_time.created
|
|
// from the event JSON file if explicit env vars are missing. It supports both
|
|
// GITEA_EVENT_PATH and GITHUB_EVENT_PATH for compatibility.
|
|
func readEventTrackedTime(logger *log.Logger) (seconds string, created string) {
|
|
paths := []string{os.Getenv("GITEA_EVENT_PATH"), os.Getenv("GITHUB_EVENT_PATH")}
|
|
for _, p := range paths {
|
|
if strings.TrimSpace(p) == "" {
|
|
continue
|
|
}
|
|
b, err := os.ReadFile(p)
|
|
if err != nil {
|
|
logger.Printf("Could not read event file '%s': %v", p, err)
|
|
continue
|
|
}
|
|
var evt map[string]any
|
|
if err := json.Unmarshal(b, &evt); err != nil {
|
|
logger.Printf("Could not parse event JSON: %v", err)
|
|
continue
|
|
}
|
|
// tracked_time is expected at top-level for time tracking related issue events
|
|
if ttRaw, ok := evt["tracked_time"]; ok {
|
|
if tt, ok := ttRaw.(map[string]any); ok {
|
|
// time
|
|
if v, ok := tt["time"]; ok {
|
|
switch t := v.(type) {
|
|
case float64:
|
|
seconds = strconv.FormatInt(int64(t), 10)
|
|
case string:
|
|
seconds = t
|
|
}
|
|
}
|
|
// created
|
|
if v, ok := tt["created"]; ok {
|
|
created, _ = v.(string)
|
|
}
|
|
}
|
|
}
|
|
if seconds != "" && created != "" {
|
|
return seconds, created
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func normalizeDate(created string) string {
|
|
created = strings.TrimSpace(created)
|
|
if created == "" {
|
|
return ""
|
|
}
|
|
// Try RFC3339
|
|
if t, err := time.Parse(time.RFC3339, created); err == nil {
|
|
return t.Format("2006-01-02")
|
|
}
|
|
// If numeric (unix seconds)
|
|
if n, err := strconv.ParseInt(created, 10, 64); err == nil {
|
|
return time.Unix(n, 0).UTC().Format("2006-01-02")
|
|
}
|
|
// Fallback: if contains T split, otherwise return as-is
|
|
if idx := strings.Index(created, "T"); idx > 0 {
|
|
return created[:idx]
|
|
}
|
|
return created
|
|
}
|
|
|
|
func main() {
|
|
baseURL := os.Getenv("BASE_URL")
|
|
branchRef := os.Getenv("BRANCH_REF")
|
|
sender := os.Getenv("SENDER_LOGIN")
|
|
title := os.Getenv("ISSUE_TITLE")
|
|
trackedSeconds := os.Getenv("TRACKED_SECONDS")
|
|
trackedCreated := os.Getenv("TRACKED_CREATED")
|
|
action := os.Getenv("EVENT_ACTION")
|
|
|
|
if baseURL == "" || sender == "" {
|
|
fmt.Println("Some required environment variables are missing (BASE_URL or SENDER_LOGIN).")
|
|
os.Exit(1)
|
|
}
|
|
if action != "add_time" && action != "edited" && action != "time_tracked" && action != "time_added" {
|
|
// Only proceed for time-related actions; allow edited if your instance sends edited
|
|
fmt.Printf("Skipping action: %s\n", action)
|
|
return
|
|
}
|
|
|
|
// Logger
|
|
logFile, err := os.OpenFile("action.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
if err != nil {
|
|
fmt.Printf("Error creating log file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer logFile.Close()
|
|
logger := log.New(io.MultiWriter(os.Stdout, logFile), "", log.LstdFlags)
|
|
|
|
logger.Printf("Event action: %s, initial TRACKED_SECONDS='%s', TRACKED_CREATED='%s'\n", action, trackedSeconds, trackedCreated)
|
|
|
|
// Ensure tracked time fields exist; otherwise, try to read from event JSON file
|
|
if strings.TrimSpace(trackedSeconds) == "" || strings.TrimSpace(trackedCreated) == "" {
|
|
sec2, crt2 := readEventTrackedTime(logger)
|
|
logger.Printf("Fallback from event file: seconds='%s', created='%s'\n", sec2, crt2)
|
|
if trackedSeconds == "" {
|
|
trackedSeconds = sec2
|
|
}
|
|
if trackedCreated == "" {
|
|
trackedCreated = crt2
|
|
}
|
|
}
|
|
if strings.TrimSpace(trackedSeconds) == "" || strings.TrimSpace(trackedCreated) == "" {
|
|
logger.Println("No tracked_time information found in event. Nothing to do.")
|
|
return
|
|
}
|
|
|
|
secondsVal, err := strconv.ParseInt(trackedSeconds, 10, 64)
|
|
if err != nil {
|
|
logger.Printf("Invalid TRACKED_SECONDS '%s': %v\n", trackedSeconds, err)
|
|
os.Exit(1)
|
|
}
|
|
hours := float64(secondsVal) / 3600.0
|
|
|
|
desc := title
|
|
if strings.TrimSpace(desc) == "" {
|
|
desc = "/"
|
|
}
|
|
|
|
date := normalizeDate(trackedCreated)
|
|
if date == "" {
|
|
logger.Println("Unable to derive date from tracked_time.created")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Determine task_id (Odoo Ticket ID) from BRANCH_REF using ticket-<number>
|
|
if strings.TrimSpace(branchRef) == "" {
|
|
logger.Println("BRANCH_REF is missing; cannot determine Odoo ticket id")
|
|
os.Exit(1)
|
|
}
|
|
var taskID int64
|
|
taskID, err = parseOdooTicketID(branchRef)
|
|
if err != nil || taskID == 0 {
|
|
logger.Printf("Failed to parse Odoo ticket from BRANCH_REF '%s': %v\n", branchRef, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
payload := TimesheetPayload{
|
|
Date: date,
|
|
Description: desc,
|
|
GiteaUsername: sender,
|
|
HourSpent: hours,
|
|
TaskID: taskID,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
logger.Printf("Error marshalling payload: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
endpoint := strings.TrimRight(baseURL, "/") + "/api/v1/account_analytic_gitea_odoo/"
|
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
logger.Printf("Error creating POST request: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
logger.Printf("Error calling FastAPI: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
logger.Printf("FastAPI status: %s\nResponse: %s\n", resp.Status, string(respBody))
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
os.Exit(1)
|
|
}
|
|
}
|