package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" "code.gitea.io/sdk/gitea" ) type TimesheetItem struct { Date string `json:"date"` TaskID int64 `json:"task_id"` GiteaUsername string `json:"gitea_username"` Description string `json:"description"` HourSpent float64 `json:"hour_spent"` } // parseOdooTicketID extracts numeric id from a ref segment like "ticket-" func parseOdooTicketID(ref string) (int64, error) { ref = strings.TrimSpace(ref) if 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):] 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- segment found") } // weekRangeMonSat returns [since, before) covering Monday 00:00 to Sunday 00:00 for the current week func weekRangeMonSat(now time.Time) (time.Time, time.Time) { loc := now.Location() t := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 0=Sunday..6=Saturday weekday := int(t.Weekday()) daysSinceMonday := (weekday + 6) % 7 // Monday=0, Sunday=6 monday := t.AddDate(0, 0, -daysSinceMonday) // Sunday 00:00 (Mon + 6 days) before := monday.AddDate(0, 0, 6) return monday, before } func main() { // 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) giteaBaseUrl := os.Getenv("GITEA_BASE_URL") fastApiBaseUrl := os.Getenv("FASTAPI_BASE_URL") fastApiToken := os.Getenv("FASTAPI_TOKEN") token := os.Getenv("GITEA_TOKEN") owner := os.Getenv("REPO_OWNER") repo := os.Getenv("REPO_NAME") if strings.TrimSpace(giteaBaseUrl) == "" || owner == "" || repo == "" || token == "" { logger.Println("Missing required env vars: GITEA_BASE_URL, REPO_OWNER, REPO_NAME, or GITEA_TOKEN") os.Exit(1) } // Gitea client giteaAPIURL := os.Getenv("GITEA_API_BASE_URL") if strings.TrimSpace(giteaAPIURL) == "" { giteaAPIURL = "https://gitea.ethumada.com" } client, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(token)) if err != nil { logger.Printf("Failed to create Gitea client: %v\n", err) os.Exit(1) } // Compute current week's Monday..Saturday window since, before := weekRangeMonSat(time.Now()) logger.Printf("Fetching tracked times from %s to %s (UTC+3)\n", since.Format(time.RFC3339), before.Format(time.RFC3339)) // Fetch all tracked times for the repository opt := gitea.ListTrackedTimesOptions{ ListOptions: gitea.ListOptions{Page: -1, PageSize: 1000}, Since: since, Before: before, } tracked, _, err := client.ListRepoTrackedTimes(owner, repo, opt) if err != nil { logger.Printf("ListRepoTrackedTimes failed: %v\n", err) os.Exit(1) } items := make([]TimesheetItem, 0, len(tracked)) for _, tt := range tracked { if tt.Issue == nil { logger.Printf("Skipping tracked time %d: no issue attached\n", tt.ID) continue } var taskID int64 if tt.Issue.Ref != "" { if id, err := parseOdooTicketID(tt.Issue.Ref); err == nil { taskID = id } else { // Fallback to issue number if ref not properly formatted taskID = tt.Issue.Index } } else { // Fallback to issue number if ref is empty taskID = tt.Issue.Index } dateStr := tt.Created.UTC().Format("2006-01-02") items = append(items, TimesheetItem{ Date: dateStr, TaskID: taskID, GiteaUsername: tt.UserName, Description: "/", HourSpent: float64(tt.Time) / 3600.0, }) } if len(items) == 0 { logger.Println("No tracked times found for the specified window. Nothing to send.") return } body, err := json.Marshal(items) if err != nil { logger.Printf("Failed to marshal payload: %v\n", err) os.Exit(1) } endpoint := strings.TrimRight(fastApiBaseUrl, "/") + "/api/v1/account_analytic_gitea_odoo" req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { logger.Printf("Failed to create request: %v\n", err) os.Exit(1) } req.Header.Set("Content-Type", "application/json") if strings.TrimSpace(fastApiToken) != "" { req.Header.Set("Authorization", "Bearer "+fastApiToken) } else { logger.Println("FASTAPI_TOKEN not provided; sending request without Authorization header") } resp, err := http.DefaultClient.Do(req) if err != nil { logger.Printf("HTTP request failed: %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) } }