Skip to content

Commit 397c3a3

Browse files
Signatures monthly report
Signed-off-by: Lukasz Gryglicki <[email protected]> Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot)
1 parent a418f4e commit 397c3a3

File tree

2 files changed

+405
-0
lines changed

2 files changed

+405
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
// Copyright The Linux Foundation.
2+
// SPDX-License-Identifier: MIT
3+
4+
package main
5+
6+
import (
7+
"encoding/csv"
8+
"fmt"
9+
"log"
10+
"os"
11+
"sort"
12+
"strconv"
13+
"strings"
14+
"time"
15+
16+
"github.com/aws/aws-sdk-go/aws"
17+
"github.com/aws/aws-sdk-go/aws/session"
18+
"github.com/aws/aws-sdk-go/service/dynamodb"
19+
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
20+
)
21+
22+
const (
23+
regionDefault = "us-east-1"
24+
profileName = "lfproduct-prod"
25+
tableName = "cla-prod-signatures"
26+
)
27+
28+
// SignatureRecord represents the DynamoDB signature record structure
29+
type SignatureRecord struct {
30+
SignatureID string `dynamodbav:"signature_id"`
31+
DateCreated string `dynamodbav:"date_created"`
32+
ApproxDateCreated string `dynamodbav:"approx_date_created"`
33+
SignatureType string `dynamodbav:"signature_type"`
34+
SigtypeSignedApprovedID string `dynamodbav:"sigtype_signed_approved_id"`
35+
SignatureApproved bool `dynamodbav:"signature_approved"`
36+
SignatureSigned bool `dynamodbav:"signature_signed"`
37+
}
38+
39+
// MonthlyStats holds the count of signatures per month
40+
type MonthlyStats struct {
41+
Month string
42+
ICLA int
43+
ECLA int
44+
CCLA int
45+
}
46+
47+
func main() {
48+
// Set up AWS session
49+
sess, err := session.NewSessionWithOptions(session.Options{
50+
Profile: profileName,
51+
Config: aws.Config{
52+
Region: aws.String(regionDefault),
53+
},
54+
})
55+
if err != nil {
56+
log.Fatalf("Error creating AWS session: %v", err)
57+
}
58+
59+
svc := dynamodb.New(sess)
60+
61+
fmt.Println("Scanning signatures table for ICLA, ECLA, and CCLA statistics...")
62+
63+
// Monthly counters map[YYYY-MM]Stats
64+
monthlyStats := make(map[string]*MonthlyStats)
65+
66+
// Scan parameters
67+
params := &dynamodb.ScanInput{
68+
TableName: aws.String(tableName),
69+
}
70+
71+
// Get current time for validation
72+
now := time.Now()
73+
currentMonth := now.Format("2006-01")
74+
75+
totalProcessed := 0
76+
totalICLA := 0
77+
totalECLA := 0
78+
totalCCLA := 0
79+
skippedInvalidDates := 0
80+
skippedFutureDates := 0
81+
82+
// Scan the table
83+
err = svc.ScanPages(params, func(page *dynamodb.ScanOutput, lastPage bool) bool {
84+
for _, item := range page.Items {
85+
var sig SignatureRecord
86+
err := dynamodbattribute.UnmarshalMap(item, &sig)
87+
if err != nil {
88+
log.Printf("Error unmarshalling record: %v", err)
89+
continue
90+
}
91+
92+
totalProcessed++
93+
if totalProcessed%1000 == 0 {
94+
fmt.Printf("Processed %d records...\n", totalProcessed)
95+
}
96+
97+
// Only process signatures that are signed and approved
98+
if !sig.SignatureSigned || !sig.SignatureApproved {
99+
continue
100+
}
101+
102+
// Get the creation date (prefer date_created, fallback to approx_date_created)
103+
creationDate := sig.DateCreated
104+
if creationDate == "" {
105+
creationDate = sig.ApproxDateCreated
106+
}
107+
if creationDate == "" {
108+
continue
109+
}
110+
111+
// Parse creation date to extract month
112+
month := extractMonth(creationDate, currentMonth)
113+
if month == "" {
114+
skippedInvalidDates++
115+
continue
116+
}
117+
118+
// Check if month is in the future
119+
if month > currentMonth {
120+
skippedFutureDates++
121+
continue
122+
}
123+
124+
// Determine signature type based on multiple factors
125+
var isICLA, isECLA, isCCLA bool
126+
127+
// Primary method: check sigtype_signed_approved_id
128+
if sig.SigtypeSignedApprovedID != "" {
129+
if strings.HasPrefix(sig.SigtypeSignedApprovedID, "icla#") {
130+
isICLA = true
131+
totalICLA++
132+
} else if strings.HasPrefix(sig.SigtypeSignedApprovedID, "ecla#") {
133+
isECLA = true
134+
totalECLA++
135+
} else if strings.HasPrefix(sig.SigtypeSignedApprovedID, "ccla#") {
136+
isCCLA = true
137+
totalCCLA++
138+
} else {
139+
// Skip unknown types
140+
continue
141+
}
142+
} else if sig.SignatureType != "" {
143+
// Fallback method: check signature_type field
144+
switch sig.SignatureType {
145+
case "cla":
146+
// For legacy CLA records without sigtype_signed_approved_id, treat as ICLA
147+
isICLA = true
148+
totalICLA++
149+
case "ccla":
150+
isCCLA = true
151+
totalCCLA++
152+
case "ecla":
153+
isECLA = true
154+
totalECLA++
155+
default:
156+
continue
157+
}
158+
} else {
159+
// Skip records without type information
160+
continue
161+
}
162+
163+
// Initialize month stats if not exists
164+
if monthlyStats[month] == nil {
165+
monthlyStats[month] = &MonthlyStats{Month: month}
166+
}
167+
168+
// Increment appropriate counter
169+
if isICLA {
170+
monthlyStats[month].ICLA++
171+
} else if isECLA {
172+
monthlyStats[month].ECLA++
173+
} else if isCCLA {
174+
monthlyStats[month].CCLA++
175+
}
176+
}
177+
return true // Continue scanning
178+
})
179+
180+
if err != nil {
181+
log.Fatalf("Error scanning table: %v", err)
182+
}
183+
184+
fmt.Printf("\nProcessing complete!\n")
185+
fmt.Printf("Total records processed: %d\n", totalProcessed)
186+
fmt.Printf("Total ICLA signatures: %d\n", totalICLA)
187+
fmt.Printf("Total ECLA signatures: %d\n", totalECLA)
188+
fmt.Printf("Total CCLA signatures: %d\n", totalCCLA)
189+
fmt.Printf("Skipped invalid dates: %d\n", skippedInvalidDates)
190+
fmt.Printf("Skipped future dates: %d\n", skippedFutureDates)
191+
192+
// Convert map to slice and sort by month
193+
var monthlyData []MonthlyStats
194+
for _, stats := range monthlyStats {
195+
monthlyData = append(monthlyData, *stats)
196+
}
197+
198+
sort.Slice(monthlyData, func(i, j int) bool {
199+
return monthlyData[i].Month < monthlyData[j].Month
200+
})
201+
202+
// Create CSV output
203+
outputFile := "signature_monthly_report.csv"
204+
file, err := os.Create(outputFile)
205+
if err != nil {
206+
log.Fatalf("Error creating output file: %v", err)
207+
}
208+
defer file.Close()
209+
210+
writer := csv.NewWriter(file)
211+
defer writer.Flush()
212+
213+
// Set semicolon as separator
214+
writer.Comma = ';'
215+
216+
// Write header
217+
writer.Write([]string{"month", "ICLAs", "ECLAs", "CCLAs"})
218+
219+
// Write data
220+
for _, stats := range monthlyData {
221+
record := []string{
222+
stats.Month,
223+
strconv.Itoa(stats.ICLA),
224+
strconv.Itoa(stats.ECLA),
225+
strconv.Itoa(stats.CCLA),
226+
}
227+
writer.Write(record)
228+
}
229+
230+
fmt.Printf("Report generated: %s\n", outputFile)
231+
fmt.Printf("Total months with activity: %d\n", len(monthlyData))
232+
}
233+
234+
// extractMonth extracts YYYY-MM from date_created field with proper validation
235+
func extractMonth(dateStr, currentMonth string) string {
236+
if dateStr == "" {
237+
return ""
238+
}
239+
240+
// Handle different date formats
241+
// 2021-08-09T15:21:56.492368+0000
242+
// 2024-07-30T12:11:34Z
243+
244+
var t time.Time
245+
var err error
246+
247+
// Try parsing different formats
248+
formats := []string{
249+
"2006-01-02T15:04:05.999999+0000",
250+
"2006-01-02T15:04:05Z",
251+
"2006-01-02T15:04:05.999999Z",
252+
"2006-01-02T15:04:05+0000",
253+
"2006-01-02T15:04:05.999999-0700",
254+
"2006-01-02T15:04:05-0700",
255+
time.RFC3339,
256+
time.RFC3339Nano,
257+
"2006-01-02 15:04:05",
258+
"2006-01-02",
259+
}
260+
261+
for _, format := range formats {
262+
t, err = time.Parse(format, dateStr)
263+
if err == nil {
264+
break
265+
}
266+
}
267+
268+
if err != nil {
269+
// Try to extract just the date part
270+
parts := strings.Split(dateStr, "T")
271+
if len(parts) > 0 {
272+
datePart := parts[0]
273+
if len(datePart) >= 7 { // YYYY-MM format at minimum
274+
// Try different date part lengths
275+
for _, length := range []int{10, 7} { // YYYY-MM-DD or YYYY-MM
276+
if len(datePart) >= length {
277+
testDateStr := datePart[:length]
278+
var testFormat string
279+
if length == 10 {
280+
testFormat = "2006-01-02"
281+
} else {
282+
testFormat = "2006-01"
283+
}
284+
285+
if testTime, testErr := time.Parse(testFormat, testDateStr); testErr == nil {
286+
// Validate year and month ranges
287+
year := testTime.Year()
288+
month := int(testTime.Month())
289+
290+
if year >= 2000 && year <= time.Now().Year() &&
291+
month >= 1 && month <= 12 {
292+
return testTime.Format("2006-01")
293+
}
294+
}
295+
}
296+
}
297+
}
298+
}
299+
return ""
300+
}
301+
302+
// Validate the parsed time
303+
year := t.Year()
304+
month := int(t.Month())
305+
306+
// Check for reasonable year and month ranges
307+
if year < 2000 || year > time.Now().Year() || month < 1 || month > 12 {
308+
return ""
309+
}
310+
311+
result := t.Format("2006-01")
312+
313+
// Additional validation: don't return invalid months like 2025-26
314+
if testTime, testErr := time.Parse("2006-01", result); testErr == nil {
315+
// Ensure the month is valid
316+
if testTime.Month() >= 1 && testTime.Month() <= 12 {
317+
return result
318+
}
319+
}
320+
321+
return ""
322+
}

0 commit comments

Comments
 (0)