@@ -268,25 +268,39 @@ class BuildEnvironment(object):
268
268
269
269
Base class for wrapping command execution for build steps. This provides a
270
270
context for command execution and reporting, and eventually performs updates
271
- on the build object itself, reporting success/failure, as well as top-level
272
- failures.
271
+ on the build object itself, reporting success/failure, as well as failures
272
+ during the context manager enter and exit.
273
+
274
+ Any exceptions raised inside this context and handled by the eventual
275
+ :py:meth:`__exit__` method, specifically, inside :py:meth:`handle_exception`
276
+ and :py:meth:`update_build`. If the exception is a subclass of
277
+ :py:cls:`BuildEnvironmentError`, then this error message is added to the
278
+ build object and is shown to the user as the top-level failure reason for
279
+ why the build failed. Other exceptions raise a general failure warning on
280
+ the build.
281
+
282
+ We only update the build through the API in one of three cases:
283
+
284
+ * The build is not done and needs incremental builds
285
+ * The build failed and we should always report this change
286
+ * The build was successful and ``commit`` is ``True``
273
287
274
288
:param project: Project that is being built
275
289
:param version: Project version that is being built
276
290
:param build: Build instance
277
291
:param record: Record status of build object
278
292
:param environment: shell environment variables
279
- :param finalize: finalize the build by setting a finished state on exit
293
+ :param commit: update the build object via API if the build was successful
280
294
"""
281
295
282
296
def __init__ (self , project = None , version = None , build = None , record = True ,
283
- environment = None , finalize = True ):
297
+ environment = None , commit = True ):
284
298
self .project = project
285
299
self .version = version
286
300
self .build = build
287
301
self .record = record
288
302
self .environment = environment or {}
289
- self .finalize = finalize
303
+ self .commit = commit
290
304
291
305
self .commands = []
292
306
self .failure = None
@@ -389,9 +403,12 @@ def done(self):
389
403
def update_build (self , state = None ):
390
404
"""Record a build by hitting the API
391
405
392
- This step is skipped if we aren't recording the build, or if we don't
393
- want to record successful builds yet (if we are running setup commands
394
- for the build)
406
+ This step is skipped if we aren't recording the build. To avoid
407
+ recording successful builds yet (for instance, running setup commands for
408
+ the build), set the ``commit`` argument on environment instantiation.
409
+
410
+ If there was an error on the build, update the build regardless of
411
+ whether ``commit`` is ``True`` or not.
395
412
"""
396
413
if not self .record :
397
414
return None
@@ -420,32 +437,41 @@ def update_build(self, state=None):
420
437
self .build ['length' ] = int (build_length .total_seconds ())
421
438
422
439
if self .failure is not None :
423
- # Only surface the error message if it was a
424
- # BuildEnvironmentException or BuildEnvironmentWarning
425
- if isinstance (self .failure ,
426
- (BuildEnvironmentException , BuildEnvironmentWarning )):
427
- self .build ['error' ] = str (self .failure )
428
- else :
429
- self .build ['error' ] = ugettext_noop (
430
- "There was a problem with Read the Docs while building your documentation. "
431
- "Please report this to us with your build id ({build_id})." .format (
432
- build_id = self .build ['id' ]
433
- )
434
- )
440
+ # Surface a generic error if the class is not a
441
+ # BuildEnvironmentError
442
+ if not isinstance (self .failure ,
443
+ (BuildEnvironmentException ,
444
+ BuildEnvironmentWarning )):
435
445
log .error (
436
446
'Build failed with unhandled exception: %s' ,
437
447
str (self .failure ),
438
- extra = {'stack' : True ,
439
- 'tags' : {'build' : self .build ['id' ]},
440
- }
448
+ extra = {
449
+ 'stack' : True ,
450
+ 'tags' : {'build' : self .build ['id' ]},
451
+ }
452
+ )
453
+ self .failure = BuildEnvironmentError (
454
+ BuildEnvironmentError .GENERIC_WITH_BUILD_ID .format (
455
+ build_id = self .build ['id' ],
456
+ )
441
457
)
458
+ self .build ['error' ] = str (self .failure )
442
459
443
460
# Attempt to stop unicode errors on build reporting
444
461
for key , val in list (self .build .items ()):
445
462
if isinstance (val , six .binary_type ):
446
463
self .build [key ] = val .decode ('utf-8' , 'ignore' )
447
464
448
- if self .finalize :
465
+ # We are selective about when we update the build object here
466
+ update_build = (
467
+ # Build isn't done yet, we unconditionally update in this state
468
+ not self .done
469
+ # Build is done, but isn't successful, always update
470
+ or (self .done and not self .successful )
471
+ # Otherwise, are we explicitly to not update?
472
+ or self .commit
473
+ )
474
+ if update_build :
449
475
try :
450
476
api_v2 .build (self .build ['id' ]).put (self .build )
451
477
except HttpClientError as e :
@@ -556,12 +582,15 @@ def __exit__(self, exc_type, exc_value, tb):
556
582
try :
557
583
client .kill (self .container_id )
558
584
except DockerAPIError :
559
- pass
585
+ log .exception (
586
+ 'Unable to remove container: id=%s' ,
587
+ self .container_id ,
588
+ )
560
589
try :
561
- log .info ('Removing container %s' , self .container_id )
590
+ log .info ('Removing container: id= %s' , self .container_id )
562
591
client .remove_container (self .container_id )
563
- # Catch direct failures from Docker API, but also requests exceptions
564
- # with the HTTP request. These should not
592
+ # Catch direct failures from Docker API or with a requests HTTP
593
+ # request. These errors should not surface to the user.
565
594
except (DockerAPIError , ConnectionError ):
566
595
log .exception (
567
596
LOG_TEMPLATE
@@ -599,13 +628,21 @@ def get_client(self):
599
628
)
600
629
return self .client
601
630
except DockerException as e :
602
- log .error (LOG_TEMPLATE
603
- .format (
604
- project = self .project .slug ,
605
- version = self .version .slug ,
606
- msg = e ),
607
- exc_info = True )
608
- raise BuildEnvironmentError ('Problem creating build environment' )
631
+ log .exception (
632
+ LOG_TEMPLATE .format (
633
+ project = self .project .slug ,
634
+ version = self .version .slug ,
635
+ msg = 'Could not connection to Docker API' ,
636
+ ),
637
+ )
638
+ # We don't raise an error here mentioning Docker, that is a
639
+ # technical detail that the user can't resolve on their own.
640
+ # Instead, give the user a generic failure
641
+ raise BuildEnvironmentError (
642
+ BuildEnvironmentError .GENERIC_WITH_BUILD_ID .format (
643
+ build_id = self .build ['id' ],
644
+ )
645
+ )
609
646
610
647
@property
611
648
def container_id (self ):
0 commit comments