@@ -2,20 +2,36 @@ import { parseArgs } from "jsr:@std/cli@1.0.14/parse-args";
22import * as mysql from "https://deno.land/x/mysql@v2.12.1/mod.ts" ;
33import { convertDsnToClientConfig } from "../../db/utils.ts" ;
44
5- interface prowJobRun {
5+ const VALID_JOB_STATES = new Set ( [
6+ "triggered" ,
7+ "pending" ,
8+ "success" ,
9+ "failure" ,
10+ "error" ,
11+ "aborted" ,
12+ ] ) ;
13+
14+ const VALID_JOB_TYPES = new Set ( [
15+ "presubmit" ,
16+ "postsubmit" ,
17+ "batch" ,
18+ "periodic" ,
19+ ] ) ;
20+
21+ export interface prowJobRun {
622 kind : string ;
723 metadata : {
8- name : string ;
9- namespace : string ;
10- labels : Record < string , string > ;
24+ name ? : string ;
25+ namespace ? : string ;
26+ labels ? : Record < string , string > ;
1127 } ;
1228 spec : {
13- type : string ;
14- agent : string ;
15- cluster : string ;
16- namespace : string ;
17- job : string ;
18- report : boolean ;
29+ type ? : string ;
30+ agent ? : string ;
31+ cluster ? : string ;
32+ namespace ? : string ;
33+ job ? : string ;
34+ report ? : boolean ;
1935 refs ?: {
2036 org : string ;
2137 repo : string ;
@@ -33,11 +49,11 @@ interface prowJobRun {
3349 } ;
3450 } ;
3551 status : {
36- state : string ;
37- startTime : string ;
38- pendingTime : string ;
39- completionTime : string ;
40- url : string ;
52+ state ? : string | null ;
53+ startTime ? : string | null ;
54+ pendingTime ? : string | null ;
55+ completionTime ? : string | null ;
56+ url ? : string | null ;
4157 } ;
4258}
4359
@@ -50,10 +66,63 @@ export async function fetchProwJobs(prowBaseUrl: string) {
5066 return data . items ;
5167}
5268
69+ function hasValidDate ( value : string | null | undefined ) : value is string {
70+ if ( ! value ) {
71+ return false ;
72+ }
73+ return ! Number . isNaN ( new Date ( value ) . getTime ( ) ) ;
74+ }
75+
76+ export function invalidProwJobReason ( job : prowJobRun ) : string | null {
77+ if ( ! job . metadata ?. namespace ) {
78+ return "missing metadata.namespace" ;
79+ }
80+ if ( ! job . metadata ?. name ) {
81+ return "missing metadata.name" ;
82+ }
83+ if ( ! job . spec ?. job ) {
84+ return "missing spec.job" ;
85+ }
86+ if ( ! job . spec ?. type ) {
87+ return "missing spec.type" ;
88+ }
89+ if ( ! VALID_JOB_TYPES . has ( job . spec . type ) ) {
90+ return `invalid spec.type: ${ job . spec . type } ` ;
91+ }
92+ if ( ! job . status ?. state ) {
93+ return "missing status.state" ;
94+ }
95+ if ( ! VALID_JOB_STATES . has ( job . status . state ) ) {
96+ return `invalid status.state: ${ job . status . state } ` ;
97+ }
98+ if ( ! hasValidDate ( job . status . startTime ) ) {
99+ return "missing or invalid status.startTime" ;
100+ }
101+ return null ;
102+ }
103+
104+ export function filterInsertableJobs ( jobs : prowJobRun [ ] ) {
105+ const insertableJobs : prowJobRun [ ] = [ ] ;
106+ const skippedJobs : { job : prowJobRun ; reason : string } [ ] = [ ] ;
107+
108+ for ( const job of jobs ) {
109+ const reason = invalidProwJobReason ( job ) ;
110+ if ( reason ) {
111+ skippedJobs . push ( { job, reason } ) ;
112+ continue ;
113+ }
114+ insertableJobs . push ( job ) ;
115+ }
116+
117+ return { insertableJobs, skippedJobs } ;
118+ }
119+
53120export async function createJobTable ( client : mysql . Client , tableName : string ) {
54121 // Validate table name to prevent SQL injection (only allow alphanumeric and underscores)
55122 if ( ! / ^ [ a - z A - Z 0 - 9 _ ] + $ / . test ( tableName ) ) {
56- throw new Error ( `Invalid table name: ${ tableName } . Only alphanumeric characters and underscores are allowed.` ) ;
123+ throw new Error (
124+ `Invalid table name: ${ tableName } . Only alphanumeric characters and underscores are allowed.` ,
125+ ) ;
57126 }
58127
59128 const sql = `
@@ -89,17 +158,22 @@ export async function createJobTable(client: mysql.Client, tableName: string) {
89158export async function migrateJobTable ( client : mysql . Client , tableName : string ) {
90159 // Validate table name to prevent SQL injection (only allow alphanumeric and underscores)
91160 if ( ! / ^ [ a - z A - Z 0 - 9 _ ] + $ / . test ( tableName ) ) {
92- throw new Error ( `Invalid table name: ${ tableName } . Only alphanumeric characters and underscores are allowed.` ) ;
161+ throw new Error (
162+ `Invalid table name: ${ tableName } . Only alphanumeric characters and underscores are allowed.` ,
163+ ) ;
93164 }
94165
95166 // Check if table exists
96167 const tableExistsResult = await client . query (
97168 `SELECT COUNT(*) as count FROM information_schema.tables
98169 WHERE table_schema = DATABASE() AND table_name = ?` ,
99- [ tableName ]
170+ [ tableName ] ,
100171 ) ;
101172
102- if ( ! tableExistsResult || tableExistsResult . length === 0 || tableExistsResult [ 0 ] . count === 0 ) {
173+ if (
174+ ! tableExistsResult || tableExistsResult . length === 0 ||
175+ tableExistsResult [ 0 ] . count === 0
176+ ) {
103177 console . info ( `Table ${ tableName } does not exist, skipping migration` ) ;
104178 return ;
105179 }
@@ -108,41 +182,43 @@ export async function migrateJobTable(client: mysql.Client, tableName: string) {
108182 const columnsResult = await client . query (
109183 `SELECT COLUMN_NAME FROM information_schema.columns
110184 WHERE table_schema = DATABASE() AND table_name = ?` ,
111- [ tableName ]
185+ [ tableName ] ,
112186 ) ;
113187
114188 const existingColumns = new Set (
115- columnsResult . map ( ( row : any ) => row . COLUMN_NAME )
189+ columnsResult . map ( ( row : { COLUMN_NAME : string } ) => row . COLUMN_NAME ) ,
116190 ) ;
117191
118192 // Add retest column if it doesn't exist
119193 if ( ! existingColumns . has ( "retest" ) ) {
120194 console . info ( `Adding column 'retest' to table ${ tableName } ` ) ;
121195 await client . execute (
122- `ALTER TABLE \`${ tableName } \` ADD COLUMN retest BOOLEAN DEFAULT NULL AFTER url`
196+ `ALTER TABLE \`${ tableName } \` ADD COLUMN retest BOOLEAN DEFAULT NULL AFTER url` ,
123197 ) ;
124198 }
125199
126200 // Add author column if it doesn't exist
127201 if ( ! existingColumns . has ( "author" ) ) {
128202 console . info ( `Adding column 'author' to table ${ tableName } ` ) ;
129203 await client . execute (
130- `ALTER TABLE \`${ tableName } \` ADD COLUMN author VARCHAR(128) AFTER retest`
204+ `ALTER TABLE \`${ tableName } \` ADD COLUMN author VARCHAR(128) AFTER retest` ,
131205 ) ;
132206 }
133207
134208 // Add event_guid column if it doesn't exist
135209 if ( ! existingColumns . has ( "event_guid" ) ) {
136210 console . info ( `Adding column 'event_guid' to table ${ tableName } ` ) ;
137211 await client . execute (
138- `ALTER TABLE \`${ tableName } \` ADD COLUMN event_guid VARCHAR(128) AFTER author`
212+ `ALTER TABLE \`${ tableName } \` ADD COLUMN event_guid VARCHAR(128) AFTER author` ,
139213 ) ;
140214 }
141215
142216 console . info ( `Migration completed for table ${ tableName } ` ) ;
143217}
144218
145219function jobInsertValues ( job : prowJobRun ) {
220+ const labels = job . metadata . labels ?? { } ;
221+
146222 // Helper to parse the retest label into a nullable boolean
147223 const parseRetestLabel = ( label : string | undefined ) : boolean | null => {
148224 if ( label === "true" ) return true ;
@@ -160,24 +236,24 @@ function jobInsertValues(job: prowJobRun) {
160236 } ;
161237
162238 return [
163- job . metadata . namespace ,
164- job . metadata . name ,
165- job . spec . job ,
166- job . spec . type ,
167- job . status . state ,
168- new Date ( job . status . startTime ) ,
239+ job . metadata . namespace ! ,
240+ job . metadata . name ! ,
241+ job . spec . job ! ,
242+ job . spec . type ! ,
243+ job . status . state ! ,
244+ new Date ( job . status . startTime ! ) ,
169245 job . status . completionTime ? new Date ( job . status . completionTime ) : null ,
170- job . metadata . labels [ "prow.k8s.io/is-optional" ] === "true" ,
246+ labels [ "prow.k8s.io/is-optional" ] === "true" ,
171247 job . spec . report || false ,
172248 job . spec . refs ?. org || null ,
173249 job . spec . refs ?. repo || null ,
174250 job . spec . refs ?. base_ref || null ,
175- job . metadata . labels [ "prow.k8s.io/refs.pull" ] || null ,
176- job . metadata . labels [ "prow.k8s.io/context" ] || null ,
251+ labels [ "prow.k8s.io/refs.pull" ] || null ,
252+ labels [ "prow.k8s.io/context" ] || null ,
177253 job . status . url || null ,
178- parseRetestLabel ( job . metadata . labels [ "prow.k8s.io/retest" ] ) ,
254+ parseRetestLabel ( labels [ "prow.k8s.io/retest" ] ) ,
179255 getAuthor ( ) ,
180- job . metadata . labels [ "event-GUID" ] || null ,
256+ labels [ "event-GUID" ] || null ,
181257 JSON . stringify ( job . spec ) ,
182258 JSON . stringify ( job . status ) ,
183259 ] ;
@@ -189,8 +265,21 @@ export async function saveJobs(
189265 jobs : prowJobRun [ ] ,
190266 chunkSize = 100 , // Default chunk size
191267) {
192- for ( let i = 0 ; i < jobs . length ; i += chunkSize ) {
193- const chunk = jobs . slice ( i , i + chunkSize ) ;
268+ const { insertableJobs, skippedJobs } = filterInsertableJobs ( jobs ) ;
269+
270+ if ( skippedJobs . length > 0 ) {
271+ console . warn (
272+ `Skipping ${ skippedJobs . length } /${ jobs . length } prow jobs with incomplete data` ,
273+ ) ;
274+ for ( const { job, reason } of skippedJobs . slice ( 0 , 5 ) ) {
275+ console . warn (
276+ `Skipped prow job ${ job . metadata ?. name ?? "<unknown>" } : ${ reason } ` ,
277+ ) ;
278+ }
279+ }
280+
281+ for ( let i = 0 ; i < insertableJobs . length ; i += chunkSize ) {
282+ const chunk = insertableJobs . slice ( i , i + chunkSize ) ;
194283 const values = chunk . map ( ( job ) => jobInsertValues ( job ) ) ;
195284 const placeholders = values . map ( ( value ) =>
196285 `(${ value . map ( ( ) => "?" ) . join ( ", " ) } )`
@@ -212,7 +301,11 @@ export async function saveJobs(
212301 ` ;
213302 const flattenedValues = values . flat ( ) ;
214303 await client . execute ( sql , flattenedValues ) ;
215- console . info ( `Saved ${ i } /${ jobs . length } jobs` ) ;
304+ console . info (
305+ `Saved ${
306+ Math . min ( i + chunkSize , insertableJobs . length )
307+ } /${ insertableJobs . length } jobs`,
308+ ) ;
216309 }
217310}
218311
0 commit comments