forked from cms-sw/cms-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
process-build-release-request
executable file
·985 lines (830 loc) · 39.7 KB
/
process-build-release-request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
#!/usr/bin/env python
from optparse import OptionParser
from github import Github
from os.path import expanduser
from categories import REQUEST_BUILD_RELEASE,CMSSW_L1,APPROVE_BUILD_RELEASE
from commands import getstatusoutput
import re
import json
import urllib2
import yaml
import os.path
from datetime import datetime, timedelta
#
# Processes a github issue to check if it is requesting the build of a new release
# If the issue is not requesting any release, it ignores it.
#
# -------------------------------------------------------------------------------
# Global Variables
# --------------------------------------------------------------------------------
GH_CMSSW_ORGANIZATION = 'cms-sw'
GH_CMSSW_REPO = 'cmssw'
GH_CMSDIST_REPO = 'cmsdist'
BUILD_REL = '^[Bb]uild[ ]+(CMSSW_[^ ]+)'
NOT_AUTHORIZED_MSG = 'You are not authorized to trigger the build of a release.'
CONFIG_MAP_FILE = 'config.map'
NO_ARCHS_FOUND_MSG = 'No architecures to build found for {rel_name}. Please check that you entered a ' \
'valid release name or that the IBs are currently enabled for {queue}'
RELEASE_BASE_URL = 'https://github.com/cms-sw/cmssw/releases/tag/%s'
RELEASE_CREATED_MSG = 'Release created: %s'
RELEASE_CREATION_FAIL_MSG = 'There was an error while attempting to create {rel_name}. ' \
'Please check if it already exists https://github.com/cms-sw/cmssw/releases'
WRONG_RELEASE_NAME_MSG = 'The release name is malformed. Please check for typos.'
ACK_MSG = 'Request received. I will start to build the release after one of the following approve ' \
'the issue: {approvers_list}. You can do this by writing "+1" in a ' \
'comment.\n You can also ask me to begin to build cmssw-tool-conf first ( Cannot be done for patch releases ). To do this write ' \
'"build cmssw-tool-conf" in a comment. I will start to build cmssw-tool-conf and then wait for the "+1" ' \
'to start the build of the release. \n'
WATCHERS_MSG = '{watchers_list} you requested to watch the automated builds for {queue}'
QUEUING_BUILDS_MSG = 'Queuing Jenkins build for the following architectures: %s \n' \
'You can abort the build by writing "Abort" in a comment. I will delete the release, ' \
'the cmssw and cmsdist tag, and close the issue. You can\'t abort the upload once at' \
' least one achitecture is being uploaded. \n' \
'If you are building cmssw-tool-conf first, I will wait for each acrhitecture to finish to start the build of cmssw.'
QUEUING_TOOLCONF_MSG = 'Queuing Jenkins build for cmssw-tool-conf for the following architectures: %s \n' \
'Be aware that I am building only cmssw-tool-conf. You still need to "+1" this issue to ' \
'make me start the build of the release. For each architecture, I will only start to build ' \
'the release after cmssw-tool-conf finishes building.'
QUEING_UPLOADS_MSG = 'Queing Jenkins upload for {architecture}'
CLEANUP_STARTED_MSG = 'The cleanup has started for {architecture}'
NOT_TOOLCONF_FOR_PATCH_MSG = 'You cannot ask me to build cmssw-tool-conf for patch releases. Please delete that message.'
JENKINS_CMSSW_X_Y_Z = 'CMSSW_X_Y_Z'
JENKINS_ARCH = 'ARCHITECTURE'
JENKINS_ISSUE_NUMBER = 'ISSUE_NUMBER'
JENKINS_MACHINE_NAME = 'MACHINE_NAME'
JENKINS_CMSSW_QUEUE = 'CMSSW_QUEUE'
JENKINS_ONLY_TOOL_CONF = 'ONLY_BUILD_TOOLCONF'
WRONG_NOTES_RELEASE_MSG = '{previous_release} does not appear to be a valid release name'
GENERATING_RELEASE_NOTES_MSG = 'Generating release notes since {previous_release}. \n' \
'You can see the progress here: \n' \
'https://cmssdt.cern.ch/jenkins/job/release-produce-changelog/\n' \
'I will generate an announcement template.\n'
PROD_ARCH_NOT_READY_MSG = 'ATTENTION!!! The production architecture ({prod_arch}) is not ready yet. '\
'This needs to be checked before asking me to generate the release notes.\n'\
'When the production architecture is uploaded successfully, I will generate the release notes.'\
' You don\'t need to write the command again.'
REL_NAME_REGEXP="(CMSSW_[0-9]+_[0-9]+)_[0-9]+(_SLHC[0-9]*|)(_pre[0-9]+|_[a-zA-Z]*patch[0-9]+|)(_[^_]*|)"
UPLOAD_COMMENT = 'upload %s'
UPLOAD_ALL_COMMENT = '^[uU]pload all$'
ABORT_COMMENT = '^[Aa]bort$'
RELEASE_NOTES_COMMENT = '^release-notes([ ]+since[ ]+[^ ]+)?$'
BUILD_TOOLCONF = '^[Bb]uild cmssw-tool-conf'
APPROVAL_COMMENT = '^[+]1$'
DEFAULT_CHECK_COMMENT = ( REQUEST_BUILD_RELEASE + [ 'cmsbuild' ] )
RELEASE_NOTES_GENERATED_LBL = 'release-notes-requested'
ANNOUNCEMENT_GENERATED_LBL = 'release-notes-requested'
JENKINS_PREV_RELEASE='PREVIOUS_RELEASE'
JENKINS_RELEASE='RELEASE'
JENKINS_PREV_CMSDIST_TAG='PREVIOUS_CMSDIST_TAG'
JENKINS_CMSDIST_TAG='CMSDIST_TAG'
INSTALLATION_FILE='/afs/.cern.ch/cms/{architecture}/tmp/{rel_name}'
ANNOUNCEMENT_TEMPLATE = 'Hi all,\n\n' \
'The {rel_type} {is_patch}release {rel_name} is now available '\
'for the following architectures:\n\n'\
'{production_arch} (production)\n'\
'{rest_of_archs}'\
'The release notes of what changed with respect to {prev_release} can be found at:\n\n'\
'https://github.com/cms-sw/cmssw/releases/{rel_name}\n'\
'{description}'\
'Cheers,\n'\
'cms-bot'
HN_REL_ANNOUNCE_EMAIL = 'hn-cms-relAnnounce@cern.ch'
ANNOUNCEMENT_EMAIL_SUBJECT = '{rel_type} {is_patch}Release {rel_name} Now Available '
MAILTO_TEMPLATE = '<a href="mailto:{destinatary}?subject={sub}&body={body}">here</a>'
# -------------------------------------------------------------------------------
# Statuses
# --------------------------------------------------------------------------------
# This is to determine the status of the issue after reading the labels
#The issue has just been created
NEW_ISSUE = 'NEW_ISSUSE'
# The issue has been received, but it needs approval to start the build
PENDING_APPROVAL = 'build-pending-approval'
# The build has been queued in jenkins
BUILD_IN_PROGRESS = 'build-in-progress'
# The build has started
BUILD_STARTED = 'build-started'
# The build has been aborted.
BUILD_ABORTED = 'build-aborted'
# they requested to build cmssw-tool-conf and it is being built
TOOLCONF_BUILDING = 'toolconf-building'
# at leas one of the architectures was built successully
BUILD_SUCCESSFUL = 'build-successful'
# the builds are being uploaded
UPLOADING_BUILDS = 'uploading-builds'
# the release has been announced
RELEASE_ANNOUNCED = 'release-announced'
# the release was build without issues.
PROCESS_COMPLETE = 'process-complete'
# -------------------------------------------------------------------------------
# Functions
# --------------------------------------------------------------------------------
#
# creates a properties file to cleanup the build files.
#
def create_properties_file_cleanup( release_name, arch, issue_number, machine_name, tool_conf=False):
if tool_conf:
out_file_name = 'cleanup-tool-conf-%s-%s.properties' % ( release_name , arch )
else:
out_file_name = 'cleanup-%s-%s.properties' % ( release_name , arch )
if opts.dryRun:
print 'Not creating cleanup properties file (dry-run):\n %s' % out_file_name
else:
print 'Creating properties file for %s' % arch
out_file = open( out_file_name , 'w' )
out_file.write( '%s=%s\n' % ( JENKINS_CMSSW_X_Y_Z , release_name ) )
out_file.write( '%s=%s\n' % ( JENKINS_ARCH , arch ) )
out_file.write( '%s=%s\n' % ( JENKINS_ISSUE_NUMBER , issue_number ) )
out_file.write( '%s=%s\n' % ( JENKINS_MACHINE_NAME , machine_name ) )
# Creates a properties file in Jenkins to trigger the upload
# it needs to know the machine that was used for the build
#
def create_properties_files_upload( release_name, arch, issue_number, machine_name ):
out_file_name = 'upload-%s-%s.properties' % ( release_name , arch )
if opts.dryRun:
print 'Not creating properties file (dry-run):\n %s' % out_file_name
else:
print 'Creating properties file for %s' % arch
out_file = open( out_file_name , 'w' )
out_file.write( '%s=%s\n' % ( JENKINS_CMSSW_X_Y_Z , release_name ) )
out_file.write( '%s=%s\n' % ( JENKINS_ARCH , arch ) )
out_file.write( '%s=%s\n' % ( JENKINS_ISSUE_NUMBER , issue_number ) )
out_file.write( '%s=%s\n' % ( JENKINS_MACHINE_NAME , machine_name ) )
#
# Searches in the comments if there is a comment made from the given users that
# matches the given pattern. It returns the date of the first comment that matches
# if no comment matches it not returns None
#
def search_date_comment( comments, user_logins, pattern, first_line ):
for comment in comments:
if comment.user.login not in user_logins:
continue
examined_str = comment.body
if first_line:
examined_str = str(comment.body.encode("ascii", "ignore").split("\n")[0].strip("\n\t\r "))
if examined_str == pattern:
return comment.created_at
if re.match( pattern , examined_str ):
return comment.created_at
return None
#
# Searches in the comments if there is a comment made from the given users that
# matches the given pattern. It returns a list with the matched comments.
#
def search_in_comments( comments, user_logins, pattern, first_line ):
found_comments = []
requested_comment_bodies = [ c.body for c in comments if c.user.login in user_logins ]
for body in requested_comment_bodies:
examined_str = body
if first_line:
examined_str = str(body.encode("ascii", "ignore").split("\n")[0].strip("\n\t\r "))
if examined_str == pattern:
found_comments.append( body )
continue
if re.match( pattern , examined_str ):
found_comments.append( body )
return found_comments
#
# Checks if the issue has already been seen so the issue will not be processed again
# Returns True if the issue needs to be processed, False if not
#
def check_if_already_processed( issue ):
comments = [ c for c in issue.get_comments( ) ]
comment_bodies = [ c.body for c in comments if c.user.login == 'cmsbuild' ]
for body in comment_bodies:
if 'Release created' in body:
return True
if 'Queuing Jenkins build' in body:
return True
if 'You are not authorized' in body:
return True
return False
#
# Creates the properties files to trigger the build in Jenkins
# if only_toolconf is selected, it adds a parameter to tell the script to only build cmssw-tool-conf
#
def create_properties_files( issue, release_name, architectures, issue_number, queue, only_toolconf=False):
if not only_toolconf:
for arch in architectures:
remove_label( issue, arch + '-tool-conf-ok' )
add_label( issue, arch + '-build-queued' )
if opts.dryRun:
print 'Not creating properties files for (dry-run): %s' % ", ".join( architectures )
return
for arch in architectures:
out_file_name = 'build-%s-%s.properties' % ( release_name , arch )
print 'Creating properties file for %s' % arch
out_file = open( out_file_name , 'w' )
out_file.write( '%s=%s\n' % ( JENKINS_CMSSW_X_Y_Z , release_name ) )
out_file.write( '%s=%s\n' % ( JENKINS_ARCH , arch ) )
out_file.write( '%s=%s\n' % ( JENKINS_ISSUE_NUMBER , issue_number ) )
out_file.write( '%s=%s\n' % ( JENKINS_CMSSW_QUEUE , queue) )
tool_conf_param = 'true' if only_toolconf else 'false'
out_file.write( '%s=%s\n' % ( JENKINS_ONLY_TOOL_CONF, tool_conf_param ) )
#
# generates the properties file for triggering the release notes
# it infers the tag names based on te format REL/<release-name>/architecture
#
def create_properties_file_rel_notes( release_name, previous_release, architecture, issue_number ):
cmsdist_tag = 'REL/'+release_name+'/'+architecture
previos_cmsdist_tag = 'REL/'+previous_release+'/'+architecture
out_file_name = 'release-notes.properties'
if opts.dryRun:
print 'Not creating properties file (dry-run): %s' % out_file_name
return
out_file = open( out_file_name , 'w' )
out_file.write( '%s=%s\n' % ( JENKINS_PREV_RELEASE, previous_release ) )
out_file.write( '%s=%s\n' % ( JENKINS_RELEASE, release_name ) )
out_file.write( '%s=%s\n' % ( JENKINS_PREV_CMSDIST_TAG, previos_cmsdist_tag ) )
out_file.write( '%s=%s\n' % ( JENKINS_CMSDIST_TAG, cmsdist_tag) )
out_file.write( '%s=%s\n' % ( JENKINS_ISSUE_NUMBER , issue_number ) )
#
# Creates a release in github
# If dry-run is selected it doesn't create the release and just prints that
# returns true if it was able to create the release, false if not
#
def create_release_github( repository, release_name, branch):
if opts.dryRun:
print 'Not creating release (dry-run):\n %s' % release_name
return True
print 'Creating release:\n %s' % release_name
# creating releases will be available in the next version of pyGithub
params = { "tag_name" : release_name,
"target_commitish" : branch,
"name" : release_name,
"body" : 'cms-bot is going to build this release',
"draft" : False,
"prerelease" : False }
request = urllib2.Request("https://api.github.com/repos/" + GH_CMSSW_ORGANIZATION + "/" + GH_CMSSW_REPO +"/releases",
headers={"Authorization" : "token " + GH_TOKEN })
request.get_method = lambda: 'POST'
print '--'
try:
print urllib2.urlopen( request, json.dumps( params ) ).read()
return True
except Exception as e:
print 'There was an error while creating the release:\n', e
return False
print
#
# Deletes in github the release given as a parameter.
# If the release does no exists, it informs it in the message.
#
def delete_release_github( release_name ):
if opts.dryRun:
print 'Not deleting release (dry-run):\n %s' % release_name
return 'Not deleting release (dry-run)'
releases_url = "https://api.github.com/repos/" + GH_CMSSW_ORGANIZATION + "/" + GH_CMSSW_REPO +"/releases?per_page=100"
request = urllib2.Request( releases_url, headers={"Authorization" : "token " + GH_TOKEN })
releases = json.loads(urllib2.urlopen(request).read())
matchingRelease = [x["id"] for x in releases if x["name"] == release_name]
if len(matchingRelease) < 1:
return "Release %s not found." % release_name
releaseId = matchingRelease[0]
url = "https://api.github.com/repos/cms-sw/cmssw/releases/%s" % releaseId
request = urllib2.Request( url, headers={"Authorization" : "token " + GH_TOKEN })
request.get_method = lambda: 'DELETE'
try:
print urllib2.urlopen( request ).read()
return 'Release successfully deleted'
except Exception as e:
return 'There was an error while deleting the release:\n %s' % e
#
# Deletes in github the tag given as a parameter
#
def delete_cmssw_tag_github( release_name ):
if opts.dryRun:
print 'Not deleting cmssw tag (dry-run):\n %s' % release_name
return 'Not deleting cmssw tag (dry-run): %s ' % release_name
cmd = "git push git@github.com:{org}/{repo}.git :{tag}"\
.format( org=GH_CMSSW_ORGANIZATION,
repo=GH_CMSSW_REPO,
tag= release_name )
print 'Executing: \n %s' % cmd
status, out = getstatusoutput( cmd )
print out
if status != 0:
msg = 'I was not able to delete the tag %s. Probaly it had not been created.' % release_name
print msg
return msg
msg = 'cmssw tag %s successfully deleted.' % release_name
return msg
#
# for each architecture, gets the tag in cmsdist that should have ben created and deletes it
#
def delete_cmsdist_tags_github( release_name, architectures ):
result = ''
for arch in architectures:
tag_to_delete = "REL/{rel_name}/{architecture}".format( rel_name=release_name, architecture=arch )
if opts.dryRun:
msg = 'Not deleting cmsdist tag (dry-run): %s' % tag_to_delete
result += '\n\n -' + msg
continue
cmd = "git push git@github.com:{org}/{repo}.git :{tag}"\
.format( org=GH_CMSSW_ORGANIZATION,
repo=GH_CMSDIST_REPO,
tag=tag_to_delete )
print 'Executing: \n %s' % cmd
status, out = getstatusoutput( cmd )
print out
if status != 0:
msg = 'I was not able to delete the cmsdist tag %s. Probably it had not been created.' % tag_to_delete
else:
msg = 'cmsdist tag %s successfully deleted.' % tag_to_delete
result += '\n\n -' + msg
return result
#
# Reads config.map and returns a list of the architectures for which a release needs to be built.
# If the list is empty it means that it didn't find any architecture for that release queue, or
# that the IBs are disabled.
#
def get_config_map_properties():
specs = []
f = open( CONFIG_MAP_FILE , 'r' )
lines = [l.strip(" \n\t;") for l in f.read().split("\n") if l.strip(" \n\t;")]
for line in lines:
entry = dict(x.split("=",1) for x in line.split(";") if x)
specs.append(entry)
return specs
#
# Adds a label to the issue in github
# if dry-run is selected it doesn't add the label and just prints it
def add_label( issue , label ):
if opts.dryRun:
print 'Not adding label (dry-run):\n %s' % label
return
print 'Adding label:\n %s' % label
issue.add_to_labels( label )
#
# posts a message to the issue in github
# if dry-run is selected it doesn't post the message and just prints it
#
def post_message( issue , msg ):
if opts.dryRun:
print 'Not posting message (dry-run):\n %s' % msg
return
if search_in_comments( comments, [ 'cmsbuild', 'nclopezo' ], msg, False):
print 'Message already in the thread: \n %s' % msg
return
print 'Posting message:\n %s' % msg
issue.create_comment( msg )
#
# reads the comments and gets returns the status of the issue
#
def get_issue_status( issue ):
labels = [ l.name for l in issue.get_labels() ]
if not labels:
return NEW_ISSUE
if BUILD_ABORTED in labels:
return BUILD_ABORTED
if PENDING_APPROVAL in labels:
return PENDING_APPROVAL
if BUILD_IN_PROGRESS in labels:
return BUILD_IN_PROGRESS
if TOOLCONF_BUILDING in labels:
return TOOLCONF_BUILDING
if BUILD_SUCCESSFUL in labels:
return BUILD_SUCCESSFUL
if UPLOADING_BUILDS in labels:
return UPLOADING_BUILDS
if RELEASE_ANNOUNCED in labels:
return RELEASE_ANNOUNCED
if PROCESS_COMPLETE in labels:
return PROCESS_COMPLETE
#
# closes the issue
#
def close_issue( issue ):
if opts.dryRun:
print 'Not closing issue (dry-run)'
return
print 'Closing issue...'
issue.edit( state="closed" )
#
# removes the labels of the issue
#
def remove_labels( issue ):
if opts.dryRun:
print 'Not removing issue labels (dry-run)'
return
issue.delete_labels()
# Removes a label form the issue
def remove_label( issue, label ):
if opts.dryRun:
print 'Not removing label (dry-run):\n %s' % label
return
if label not in ALL_LABELS:
print 'label ', label, ' does not exist. Not attempting to remove'
return
print 'Removing label: %s' % label
issue.remove_from_labels( label )
#
# Aborts the build:
# -Deletes the release in github
# -Deletes the cmssw tags
# -Deletes the cmsdist tags
#
def abort_build( issue, release_name, architectures):
msg = 'Deleting %s:' % release_name
del_rel_result = delete_release_github( release_name )
msg += '\n\n -' + del_rel_result
msg += '\n\n -' + delete_cmssw_tag_github( release_name )
msg += delete_cmsdist_tags_github( release_name, architectures )
msg += '\n\n' + 'You must create a new issue to start over the build.'
post_message( issue, msg )
#
# Classifies the labels and fills the lists with the details of the current
# status of each architecture
#
def fillDeatilsArchsLists( issue ):
labels = [ l.name for l in issue.get_labels() ]
BUILD_OK.extend( [ x.split('-')[0] for x in labels if '-build-ok' in x ] )
UPLOAD_OK.extend( [ x.split('-')[0] for x in labels if '-upload-ok' in x ] )
UPLOADING.extend( [ x.split('-')[0] for x in labels if '-uploading' in x ] )
BUILD_ERROR.extend( [ x.split('-')[0] for x in labels if '-build-error' in x ] )
TOOL_CONF_BUILDING.extend( [ x.split('-')[0] for x in labels if '-tool-conf-building' in x ] )
TOOL_CONF_OK.extend( [ x.split('-')[0] for x in labels if '-tool-conf-ok' in x ] )
TOOL_CONF_ERROR.extend( [ x.split('-')[0] for x in labels if '-tool-conf-error' in x ] )
TOOL_CONF_WAITING.extend( [ x.split('-')[0] for x in labels if '-tool-conf-waiting' in x ] )
TO_CLEANUP.extend( UPLOAD_OK + BUILD_ERROR + BUILD_OK )
#
# Triggers the cleanup for the architectures in the list TO_CLEANUP
#
def triggerCleanup( issue, comments, release_name ):
if TO_CLEANUP:
for arch in TO_CLEANUP:
pattern = 'The build has started for %s .*' % arch
build_info_comments = search_in_comments( comments, ['cmsbuild', 'nclopezo'], pattern, False)
pattern_tool_conf = 'The cmssw-tool-conf build has started for %s .*' % arch
tool_conf_info_comments = search_in_comments( comments, ['cmsbuild', 'nclopezo'], pattern_tool_conf, False)
if not build_info_comments:
print 'No information found about the build machine, something is wrong for %s' % arch
continue
build_machine = build_info_comments[-1].split( ' ' )[7].strip( '.' )
print '\nTriggering cleanup for %s' % arch
create_properties_file_cleanup( release_name, arch, issue.number, build_machine )
if tool_conf_info_comments:
build_machine_toolconf = tool_conf_info_comments[-1].split( ' ' )[8].strip( '.' )
print '\nTriggering tool-conf cleanup for %s' % arch
create_properties_file_cleanup( release_name, arch, issue.number, build_machine_toolconf, tool_conf=True)
print
msg = CLEANUP_STARTED_MSG.format( architecture=arch )
post_message( issue, msg )
remove_label( issue, arch + '-upload-ok' )
remove_label( issue, arch + '-build-error' )
add_label( issue, arch + '-finished' )
#
# Creates the release in github, including the cmssw tag. It then creates the files to trigger the builds in jenkins
#TODO check cmssw toolconf
#
def start_release_build( issue, release_name, release_branch, architectures ):
# if someone approved, go ahead and create the release
cmssw_repo = gh.get_organization( GH_CMSSW_ORGANIZATION ).get_repo( GH_CMSSW_REPO )
release_created = create_release_github( cmssw_repo, release_name, release_branch )
if not release_created:
msg = RELEASE_CREATION_FAIL_MSG.format( rel_name=release_name )
post_message( issue , RELEASE_CREATION_FAIL_MSG.format( rel_name=release_name ) )
exit( 0 )
msg = RELEASE_CREATED_MSG % ( RELEASE_BASE_URL % release_name )
post_message( issue , msg )
ready_to_build = list( set( architectures ) - set( TOOL_CONF_WAITING ) - set( TOOL_CONF_ERROR ) - set( TOOL_CONF_BUILDING ) )
create_properties_files( issue, release_name, ready_to_build, issue.number, release_queue )
if ready_to_build:
msg = QUEUING_BUILDS_MSG % ', '.join( ready_to_build )
post_message( issue , msg )
#
# Creates the files to trigger the build of cmssw-tool-conf in jenkins.
#
def start_tool_conf_build( issue, release_name, release_branch, architectures ):
create_properties_files( issue, release_name, architectures, issue.number, release_queue, only_toolconf=True )
msg = QUEUING_TOOLCONF_MSG % ', '.join( architectures )
post_message( issue , msg )
#
# removes the label for the current state and adds the label for the next state
#
def go_to_state( issue, current_state, new_state ):
print '\nSwitching to state: ', new_state, '\n'
remove_label( issue, current_state )
add_label( issue, new_state )
#
# Generates an announcement prototype
#
def generate_announcement( release_name, previous_release_name, production_architecture, architectures ):
print '\nGenerating announcement template...\n'
is_development = 'pre' in release_name
type_str = 'development' if is_development else 'production'
print 'Is development: ', is_development
is_patch = 'patch' in release_name
patch_str = 'patch ' if is_patch else ''
print 'Is patch: ', is_patch
# The description of the issue should explain the reason for building the release
desc = '\n' + issue.body + '\n\n' if issue.body else '\n'
print 'Description: \n', desc
architectures.remove( production_architecture )
rest_of_archs = '\n'.join(architectures) + '\n\n' if architectures else '\n'
announcement = ANNOUNCEMENT_TEMPLATE.format( rel_type=type_str,
is_patch=patch_str,
rel_name=release_name,
production_arch=production_architecture,
rest_of_archs=rest_of_archs,
prev_release=previous_release_name,
description=desc )
return announcement
#
# Generates a link that the uset can click to write the announcement email with just one click
#
def generate_announcement_link( announcement, release_name ):
is_development = 'pre' in release_name
type_str = 'Development' if is_development else 'Production'
is_patch = 'patch' in release_name
patch_str = 'patch ' if is_patch else ''
subject = ANNOUNCEMENT_EMAIL_SUBJECT.format( rel_type=type_str,
is_patch=patch_str,
rel_name=release_name).replace( ' ', '%20' )
msg = announcement.replace( '\n', '%0D%0A' ).replace( ' ', '%20')
link = MAILTO_TEMPLATE.format( destinatary=HN_REL_ANNOUNCE_EMAIL,
sub=subject,
body=msg )
return link
#
# checks if the production architecture is ready, if so, it generates a template for the announcement
#
def check_if_prod_arch_ready( issue, prev_rel_name, production_architecture ):
if ( production_architecture in UPLOAD_OK ):
print 'Production architecture successfully uploaded..'
#For now, it assumes that the release is being installed and it will be installed successfully
announcement = generate_announcement( release_name, prev_rel_name, production_architecture, UPLOAD_OK )
mailto = generate_announcement_link( announcement, release_name )
msg = 'You can use this template for announcing the release:\n\n%s\n\n' \
'You can also click %s to send the email.' % (announcement, mailto)
post_message( issue, msg )
add_label( issue, ANNOUNCEMENT_GENERATED_LBL )
#
# checks the issue for archs to be uploaded
#
def check_archs_to_upload( release_name, issue ):
print 'Looking for archs ready to be uploaded...\n'
for arch in BUILD_OK:
print 'Ready to upload %s' % arch
pattern = '^The build has started for %s .*' % arch
build_info_comments = search_in_comments( comments, ['cmsbuild','nclopezo'] , pattern, True )
if not build_info_comments:
print 'No information found about the build machine, something is wrong'
exit( 1 )
first_line_info_comment = str(build_info_comments[-1].encode("ascii", "ignore").split("\n")[0].strip("\n\t\r "))
build_machine = first_line_info_comment.split( ' ' )[ 7 ].strip( '.' )
print 'Triggering upload for %s' % arch
create_properties_files_upload( release_name , arch , issue.number , build_machine )
post_message( issue , QUEING_UPLOADS_MSG.format( architecture=arch ) )
remove_label( issue, arch + '-build-ok' )
add_label( issue, arch + '-uploading' )
if BUILD_OK:
return True
else:
return False
#
# checks if there are architectures that are ready to be built afer building tool-conf, and triggers the build if neccessary
#
def check_to_build_after_tool_conf( issue, release_name, release_queue):
print 'Checking if there are architectures waiting to be started after building tool-conf'
ready_to_build = TOOL_CONF_OK
print ready_to_build
create_properties_files( issue, release_name, ready_to_build, issue.number, release_queue )
if ready_to_build:
msg = QUEUING_BUILDS_MSG % ', '.join( ready_to_build )
post_message( issue , msg )
#
# Guesses the previous release name based on the name given as a parameter
#
def guess_prev_rel_name( release_name ):
num_str = release_name.split( '_' )[ -1 ]
number = int( re.search( '[0-9]+$', release_name).group(0) )
prev_number = number - 1
prev_num_str = num_str.replace( str(number), str(prev_number) )
if ('patch' in release_name) or ('pre' in release_name):
if prev_number < 1:
return release_name.replace( '_' + num_str, '')
else:
if prev_number < 0:
return release_name + '_pre9'
return release_name.replace( num_str, prev_num_str)
# -------------------------------------------------------------------------------
# Start of execution
# --------------------------------------------------------------------------------
if __name__ == "__main__":
parser = OptionParser( usage="%prog <issue-id>" )
parser.add_option( "-n" , "--dry-run" , dest="dryRun" , action="store_true", help="Do not post on Github", default=False )
parser.add_option( "-f" , "--force" , dest="force" , action="store_true", help="Ignore previous comments in the issue and proccess it again", default=False )
parser.add_option( "-c", "--check-upload", dest="check_upload" , action="store" , help="Check if one of the authorized users has written the upload message"
"for the architecture given as a parameter. It exits with 0 if it finds"
"a message with the structure 'upload <architecture>', if not it exits"
" with 1" )
opts, args = parser.parse_args( )
if len( args ) != 1:
parser.print_help()
parser.error( "Too many arguments" )
GH_TOKEN = open( expanduser("~/.github-token")).read().strip()
issue_id = int( args[ 0 ] )
gh = Github( login_or_token=GH_TOKEN )
rate_limit = gh.get_rate_limit().rate
print 'API Rate Limit'
print 'Limit: ', rate_limit.limit
print 'Remaining: ', rate_limit.remaining
print 'Reset time (GMT): ', rate_limit.reset
cmssw_repo = gh.get_repo( GH_CMSSW_ORGANIZATION + '/' + GH_CMSSW_REPO )
issue = cmssw_repo.get_issue( issue_id )
ALL_LABELS = [ l.name for l in cmssw_repo.get_labels() ]
comments = [ c for c in issue.get_comments( ) ]
# 1. Is this a pull request?
if issue.pull_request:
print 'This is a pull request, ignoring.'
exit( 0 )
title_match = re.match(BUILD_REL, issue.title)
# 2. Is this issue meant to build a release?
if not title_match:
print 'This issue is not for building a release, ignoring.'
exit( 0 )
# 3. Is the author authorized to trigger a build?
if not issue.user.login in REQUEST_BUILD_RELEASE:
print 'User not authorized'
post_message( issue , NOT_AUTHORIZED_MSG )
exit( 0 )
release_name = title_match.group(1)
# Get the release queue from the release name.
print release_name
rel_name_match = re.match( REL_NAME_REGEXP, release_name )
if not rel_name_match:
print 'Release name not correctly formed'
post_message( issue, WRONG_RELEASE_NAME_MSG )
exit( 0 )
release_queue = "".join([x for x in rel_name_match.group(1,4)] + ["_X"] + [x.strip("0123456789") for x in rel_name_match.group(2)])
print release_queue
specs = get_config_map_properties()
architectures = [x["SCRAM_ARCH"] for x in specs
if x["RELEASE_QUEUE"] == release_queue and not "DISABLED" in x]
# Check if we have at least one architecture to build and complain if not.
if not architectures:
print 'no archs found for the requested release'
msg = NO_ARCHS_FOUND_MSG.format( rel_name=release_name, queue=release_queue )
post_message( issue, msg )
exit( 0 )
# Determine the release branch (which is the same as the release queue if not
# specified) and start the build if needed.
release_branches = [x["RELEASE_BRANCH"] for x in specs
if x["RELEASE_QUEUE"] == release_queue and "RELEASE_BRANCH" in x and not "DISABLED" in x]
possible_prod_arch = [x["SCRAM_ARCH"] for x in specs
if x["RELEASE_QUEUE"] == release_queue and not "DISABLED" in x and "PROD_ARCH" in x]
production_architecture = possible_prod_arch[0] if possible_prod_arch else architectures[0]
release_branch = release_queue
if len(release_branches):
release_branch = release_branches[0]
# Get the status of this issue.
status = get_issue_status( issue )
print 'Status: %s \n' % status
labels = [ l.name for l in issue.get_labels() ]
BUILD_OK = []
UPLOAD_OK = []
UPLOADING = []
BUILD_ERROR = []
TO_CLEANUP = []
TOOL_CONF_BUILDING = []
TOOL_CONF_OK = []
TOOL_CONF_ERROR = []
TOOL_CONF_WAITING = []
# These lists are filled by fillDeatilsArchsLists( issue )
fillDeatilsArchsLists( issue )
if status == BUILD_ABORTED:
print 'Build Aborted. A new issue must be created if you want to build the release'
date_aborted = search_date_comment( comments, APPROVE_BUILD_RELEASE, ABORT_COMMENT, True )
# the time is 2 days because a new issue must be created to start again the build
# if for the new build the build starts in the same machine as before, this will
# start to delete the work directory of the new build.
cleanup_deadline = datetime.now() - timedelta(days=2)
if date_aborted < cleanup_deadline:
print 'Cleaning up since it is too old since it was aborted'
triggerCleanup( issue, comments, release_name )
close_issue( issue )
else:
print 'Not too old yet to clean up'
if status == NEW_ISSUE:
approvers = ", ".join( [ "@"+x for x in APPROVE_BUILD_RELEASE ] )
ALL_WATCHERS = (yaml.load(file("build-release-watchers.yaml")))
watchers = ALL_WATCHERS.get( release_queue )
msg = ACK_MSG.format( approvers_list=approvers )
if watchers:
watchers_l = ", ".join( [ "@"+x for x in watchers ] )
watchers_msg = WATCHERS_MSG.format( watchers_list=watchers_l, queue=release_queue )
msg += watchers_msg
post_message( issue, msg)
add_label( issue, PENDING_APPROVAL )
exit( 0 )
if status == PENDING_APPROVAL:
approval_comments = search_in_comments( comments, APPROVE_BUILD_RELEASE , APPROVAL_COMMENT, True )
build_toolconf_commments = search_in_comments( comments, APPROVE_BUILD_RELEASE , BUILD_TOOLCONF, True )
is_patch = 'patch' in release_name
if build_toolconf_commments:
if is_patch:
post_message( issue, NOT_TOOLCONF_FOR_PATCH_MSG )
else:
start_tool_conf_build( issue, release_name, release_branch, architectures )
go_to_state( issue, status, TOOLCONF_BUILDING )
elif approval_comments:
start_release_build( issue, release_name, release_branch, architectures )
go_to_state( issue, status, BUILD_IN_PROGRESS )
else:
print 'Build not approved or cmssw-tool-conf not requested yet'
exit( 0 )
if status == TOOLCONF_BUILDING:
print 'Waiting for approval to start the build'
approval_comments = search_in_comments( comments, APPROVE_BUILD_RELEASE , APPROVAL_COMMENT, True )
if approval_comments:
print 'Build approved, switching to "Build in Progress" state'
# add a label for each arch for which tool conf has not started in jenkins
tool_conf_reported = ( TOOL_CONF_BUILDING + TOOL_CONF_OK + TOOL_CONF_ERROR )
not_started = list( set( architectures ) - set( tool_conf_reported ) )
for arch in not_started:
add_label( issue, arch + '-tool-conf-waiting' )
TOOL_CONF_WAITING.append( arch )
go_to_state( issue, status, BUILD_IN_PROGRESS )
start_release_build( issue, release_name, release_branch, architectures )
if status == BUILD_IN_PROGRESS:
abort_comments = search_in_comments( comments , APPROVE_BUILD_RELEASE , ABORT_COMMENT, True )
print abort_comments
if abort_comments:
print 'Aborting'
abort_build( issue, release_name, architectures )
go_to_state( issue, status, BUILD_ABORTED )
exit( 0 )
# if the previous state was to build tool-conf there are architectures for which it is needed to wait
build_toolconf_commments = search_in_comments( comments, APPROVE_BUILD_RELEASE , BUILD_TOOLCONF, True )
if build_toolconf_commments:
check_to_build_after_tool_conf( issue, release_name, release_queue)
if BUILD_OK:
go_to_state( issue, status, BUILD_SUCCESSFUL )
if status == BUILD_SUCCESSFUL:
abort_comments = search_in_comments( comments , APPROVE_BUILD_RELEASE , ABORT_COMMENT, True )
print abort_comments
if abort_comments:
print 'Aborting'
abort_build( issue, release_name, architectures )
go_to_state( issue, status, BUILD_ABORTED )
exit( 0 )
# if the previous state was to build tool-conf there are architectures for which it is needed to wait
build_toolconf_commments = search_in_comments( comments, APPROVE_BUILD_RELEASE , BUILD_TOOLCONF, True )
if build_toolconf_commments:
check_to_build_after_tool_conf( issue, release_name, release_queue)
upload_all_requested = search_in_comments( comments, APPROVE_BUILD_RELEASE, UPLOAD_ALL_COMMENT, True )
if upload_all_requested:
check_archs_to_upload( release_name, issue )
go_to_state( issue, status, UPLOADING_BUILDS )
else:
print 'Upload not requested yet'
if status == UPLOADING_BUILDS:
#upload archs as soon as they get ready
check_archs_to_upload( release_name, issue )
#Check if someone asked for release notes, go to next state after generating notes.
#At least one architecture must have been successfully uploaded
if UPLOAD_OK and ( RELEASE_NOTES_GENERATED_LBL not in labels ):
print 'checking if someone asked for the release notes'
release_notes_comments = search_in_comments( comments, APPROVE_BUILD_RELEASE, RELEASE_NOTES_COMMENT, True )
if release_notes_comments:
comment = release_notes_comments[-1]
first_line = str(comment.encode("ascii", "ignore").split("\n")[0].strip("\n\t\r "))
comment_parts = first_line.strip().split(' ')
if len( comment_parts ) > 1:
prev_rel_name = comment_parts[ 2 ].rstrip()
else:
prev_rel_name = guess_prev_rel_name( release_name )
print prev_rel_name
rel_name_match = re.match( REL_NAME_REGEXP, prev_rel_name )
if not rel_name_match:
msg = WRONG_NOTES_RELEASE_MSG.format( previous_release=prev_rel_name )
post_message( issue, msg )
exit( 0 )
if ( production_architecture not in UPLOAD_OK ):
msg = PROD_ARCH_NOT_READY_MSG.format( prod_arch=production_architecture )
post_message( issue, msg )
exit( 0 )
create_properties_file_rel_notes( release_name, prev_rel_name, production_architecture, issue.number )
msg = GENERATING_RELEASE_NOTES_MSG.format( previous_release=prev_rel_name )
post_message( issue, msg )
add_label( issue, RELEASE_NOTES_GENERATED_LBL )
#Check if the production architecture was uploaded and was correctly installed, generate announcement if so.
check_if_prod_arch_ready( issue, prev_rel_name, production_architecture )
go_to_state( issue, status, RELEASE_ANNOUNCED )
if status == RELEASE_ANNOUNCED:
#upload archs as soon as they get ready
check_archs_to_upload( release_name, issue )
# check if the cleanup has been requested or if 2 days have passed since the release-notes were generated.
print 'Checking if someone requested cleanup, or the issue is too old...'
date_rel_notes = search_date_comment( comments, APPROVE_BUILD_RELEASE, RELEASE_NOTES_COMMENT, True )
cleanup_deadline = datetime.now() - timedelta(days=2)
if date_rel_notes:
too_old = date_rel_notes < cleanup_deadline
else:
too_old = False
pattern = '^cleanup$'
cleanup_requested_comments = search_in_comments( comments, APPROVE_BUILD_RELEASE, pattern, True )
if cleanup_requested_comments or too_old:
triggerCleanup( issue, comments, release_name )
close_issue( issue )
go_to_state( issue, status, PROCESS_COMPLETE )