Coverage for src/jobs/runner.py: 100.00%
355 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-18 15:02 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-18 15:02 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from argparse import Namespace
5from os import chmod, environ, stat, system
6from pathlib import Path, PurePosixPath
7from re import sub as regex_sub
8from signal import getsignal, SIGINT, signal, SIGTERM
9from stat import S_IRGRP, S_IROTH, S_IXGRP, S_IXOTH, S_IXUSR
10from subprocess import run
11from sys import stdout
12from time import sleep
13from types import FrameType
14from typing import Dict, List, Optional
16# Components
17from ..engines.engine import Engine, privileged_default
18from ..models.flags import Flags
19from ..models.images import Entrypoint
20from ..models.jobs import Job
21from ..package.bundle import Bundle
22from ..prints.histories import PipelineHistory
23from ..system.git import Git
24from ..system.platform import Platform
25from ..system.xauth import Xauth
26from ..types.environment import Environment
27from ..types.paths import Paths
28from ..types.strings import Strings
29from ..types.volumes import Volumes
30from .outputs import Outputs
31from .scripts import ScriptsFile
33# Runner class, pylint: disable=too-few-public-methods
34class Runner:
36 # Constants
37 __MARKER_DEBUG: str = '__GITLAB_CI_LOCAL_DEBUG__'
38 __MARKER_ENDING: str = '__GITLAB_CI_LOCAL_ENDING__'
39 __MARKER_RESULT: str = '__GITLAB_CI_LOCAL_RESULT__'
41 # Members
42 __engines: Dict[str, Optional[Engine]] = {}
43 __interrupted: bool
44 __options: Namespace
46 # Constructor
47 def __init__(
48 self,
49 options: Namespace,
50 ) -> None:
52 # Prepare flags
53 self.__interrupted = False
55 # Prepare options
56 self.__options = options
58 # Run container, pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-positional-arguments,too-many-statements
59 def __run_container(
60 self,
61 engine: Optional[Engine],
62 variables: Dict[str, str],
63 path_parent: str,
64 target_parent: str,
65 image: str,
66 job: Job,
67 script_file: ScriptsFile,
68 entrypoint: Entrypoint,
69 network: str,
70 privileged: bool,
71 target_workdir: str,
72 last_result: bool,
73 result: bool,
74 ) -> bool:
76 # Validate engine
77 assert engine is not None
79 # Configure engine variables
80 variables[Bundle.ENV_ENGINE_NAME] = engine.name
82 # Prepare volumes mounts
83 volumes = Volumes()
85 # Mount repository folder
86 volumes.add(
87 path_parent,
88 target_parent,
89 'rw',
90 True,
91 )
93 # Extend mounts
94 if self.__options.volume:
95 for volume in self.__options.volume:
97 # Handle .local volumes
98 cwd = Path('.')
99 volume_local = False
100 if volume.startswith(Volumes.LOCAL_FLAG):
101 cwd = self.__options.path
102 volume_local = True
103 volume = volume[len(Volumes.LOCAL_FLAG):]
105 # Parse volume fields
106 volume_nodes = Volumes.parse(volume)
108 # Parse HOST:TARGET:MODE, pylint: disable=line-too-long
109 if len(volume_nodes) == 3:
110 volume_host = Paths.resolve(cwd / Paths.expand(volume_nodes[0]))
111 volume_target = Paths.expand(volume_nodes[1], home=False)
112 volume_mode = volume_nodes[2]
114 # Parse HOST:TARGET, pylint: disable=line-too-long
115 elif len(volume_nodes) == 2:
116 volume_host = Paths.resolve(cwd / Paths.expand(volume_nodes[0]))
117 volume_target = Paths.expand(volume_nodes[1], home=False)
118 volume_mode = 'rw'
120 # Parse VOLUME, pylint: disable=line-too-long
121 else:
122 volume_host = Paths.resolve(cwd / Paths.expand(volume_nodes[0]))
123 volume_target = Paths.resolve(cwd / Paths.expand(volume_nodes[0]))
124 volume_mode = 'rw'
126 # Append volume mounts
127 volumes.add(
128 volume_host,
129 volume_target,
130 volume_mode,
131 not volume_local,
132 )
134 # Image validation
135 if not image: # pragma: no cover
136 raise ValueError(f"Missing image for \"{job.stage} / {job.name}\"")
137 engine.get(image)
139 # Display option
140 if Flags.check_bool(
141 option=self.__options.display,
142 flag=Bundle.FLAG_DISPLAY,
143 variables=job.variables,
144 default=False,
145 ):
147 # Bind display environment
148 if Platform.ENV_DISPLAY in environ: # pragma: no cover
149 variables[Platform.ENV_DISPLAY] = environ[Platform.ENV_DISPLAY]
150 if Platform.ENV_XAUTHORITY in environ: # pragma: no cover
151 variables[Platform.ENV_XAUTHORITY] = environ[Platform.ENV_XAUTHORITY]
153 # Bind display socket
154 for display_socket in Platform.display(): # pragma: no cover
155 volumes.add(
156 display_socket,
157 display_socket,
158 'rw',
159 False,
160 )
162 # Bind display Xauthority
163 display_xauth_magic: str = Xauth.magic()
164 if display_xauth_magic: # pragma: no cover
165 variables[Bundle.ENV_DISPLAY_XAUTH_MAGIC_ADD] = display_xauth_magic
167 # SSH option
168 ssh: str = Flags.check_strbool(
169 option=self.__options.ssh,
170 flag=Bundle.FLAG_SSH,
171 variables=job.variables,
172 default='',
173 const=Bundle.ARGUMENT_SSH_USER_DEFAULT,
174 )
175 if ssh:
177 # Bind SSH credentials
178 volumes.add(
179 Paths.expand('~/.ssh'),
180 Paths.home(ssh) + '/.ssh',
181 'ro',
182 False,
183 )
185 # Bind SSH agent
186 if Platform.ENV_SSH_AUTH_SOCK in environ:
187 if environ[Platform.ENV_SSH_AUTH_SOCK]:
188 volumes.add(
189 Paths.expand(environ[Platform.ENV_SSH_AUTH_SOCK]),
190 Paths.expand(environ[Platform.ENV_SSH_AUTH_SOCK]),
191 'ro',
192 True,
193 )
194 variables[Platform.ENV_SSH_AUTH_SOCK] = environ[
195 Platform.ENV_SSH_AUTH_SOCK]
197 # Launch container
198 engine.run(
199 image=image,
200 commands=[script_file.target()],
201 entrypoint=entrypoint,
202 variables=variables,
203 network=network,
204 option_privileged=privileged,
205 option_sockets=job.options.sockets,
206 services=bool(job.services),
207 volumes=volumes,
208 directory=target_workdir,
209 temp_folder=script_file.folder,
210 )
212 # Create interruption handler
213 def interrupt_handler(
214 __signalnum: Optional[int] = None,
215 __frame: Optional[FrameType] = None,
216 ) -> None:
217 self.__interrupted = True
218 Outputs.interruption()
219 assert engine is not None
220 engine.stop(0)
222 # Register interruption handler
223 handler_int_original = getsignal(SIGINT)
224 handler_term_original = getsignal(SIGTERM)
225 signal(SIGINT, interrupt_handler)
226 signal(SIGTERM, interrupt_handler)
228 # Execution wrapper
229 marker_result: Optional[bool] = None
230 success = False
232 # Show container logs
233 for line in engine.logs():
234 if isinstance(line, bytes):
235 line_decoded = line.decode()
236 if self.__MARKER_DEBUG in line_decoded:
237 break # pragma: no cover
238 if self.__MARKER_RESULT in line_decoded:
239 marker_result = int(
240 line_decoded.split(f'{self.__MARKER_RESULT}:', 1)[1]) == 0
241 continue
242 if self.__MARKER_ENDING in line_decoded:
243 break
244 stdout.buffer.write(line)
245 Platform.flush()
247 # Runner bash or debug mode
248 if not self.__interrupted and (Flags.check_bool(
249 option=self.__options.bash,
250 flag=Bundle.FLAG_BASH,
251 variables=job.variables,
252 default=False,
253 ) or Flags.check_bool(
254 option=self.__options.debug,
255 flag=Bundle.FLAG_DEBUG,
256 variables=job.variables,
257 default=False,
258 )):
260 # Select shell
261 shell: str = ''
262 if Flags.has(
263 option=self.__options.shell,
264 flag=Bundle.FLAG_SHELL,
265 variables=job.variables,
266 ):
267 shell = Flags.check_str(
268 option=self.__options.shell,
269 flag=Bundle.FLAG_SHELL,
270 variables=job.variables,
271 default='sh',
272 )
273 elif engine.supports('bash'):
274 shell = 'bash'
275 else:
276 shell = 'sh'
278 # Select console
279 console: bool = Platform.IS_TTY_STDIN and not Flags.check_bool(
280 option=self.__options.no_console,
281 flag=Bundle.FLAG_NO_CONSOLE,
282 variables=job.variables,
283 default=False,
284 )
286 # Acquire container informations
287 container_exec = engine.cmd_exec()
288 container_name = engine.container
290 # Debugging informations
291 Outputs.debugging(
292 container_exec,
293 container_name,
294 shell,
295 console=console,
296 )
298 # Launch container console
299 if console:
300 run(
301 f'{container_exec} {container_name} {shell}',
302 check=False,
303 shell=True,
304 )
305 self.__interrupted = True
306 engine.stop(0)
308 # Check container status
309 success = engine.wait()
311 # Stop container
312 engine.stop(0)
313 sleep(0.1)
315 # Remove container
316 engine.remove()
318 # Unregister interruption handler
319 signal(SIGINT, handler_int_original)
320 signal(SIGTERM, handler_term_original)
322 # Result evaluation
323 if job.when in [
324 'on_failure',
325 'always',
326 ]:
327 result = last_result
328 elif success:
329 result = True
330 elif self.__interrupted and marker_result is not None:
331 result = marker_result
332 return result
334 # Run native, pylint: disable=too-many-arguments,too-many-positional-arguments
335 @staticmethod
336 def __run_native(
337 variables: Dict[str, str],
338 entrypoint: Entrypoint,
339 script_file: ScriptsFile,
340 job: Job,
341 last_result: bool,
342 result: bool,
343 ) -> bool:
345 # Configure host variables
346 variables[Bundle.ENV_HOST] = 'true'
348 # Prepare environment
349 _environ = dict(environ)
350 environ.update(variables)
352 # Detect nested execution
353 if ScriptsFile.SHEBANG_MARKER_BASH in environ:
354 del environ[ScriptsFile.SHEBANG_MARKER_BASH]
356 # Native execution
357 scripts: List[str] = []
358 if entrypoint:
359 scripts += entrypoint
360 if not scripts:
361 scripts = ['sh']
362 scripts += [f'"{script_file.name}"']
363 success = system(' '.join(scripts)) == 0
365 # Result evaluation
366 if job.when in [
367 'on_failure',
368 'always',
369 ]:
370 result = last_result
371 elif success:
372 result = True
374 # Restore environment
375 environ.clear()
376 environ.update(_environ)
378 # Result
379 return result
381 # Run, pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements
382 def run(
383 self,
384 job: Job,
385 last_result: bool,
386 pipeline_history: PipelineHistory,
387 ) -> bool:
389 # Variables
390 engine: Optional[Engine] = None
391 engines_names: str
392 engine_type: str = ''
393 host: bool = False
394 network: str
395 privileged: bool
396 quiet: bool
397 random_paths: bool = False
398 real_paths: bool = False
399 result: bool = False
400 script_file: ScriptsFile
402 # Prepare history
403 job_history = pipeline_history.add(
404 job.stage,
405 job.name,
406 )
408 # Validate paths
409 if Flags.check_bool(
410 option=self.__options.random_paths,
411 flag=Bundle.FLAG_RANDOM_PATHS,
412 variables=job.variables,
413 default=False,
414 ) and Flags.check_bool(
415 option=self.__options.real_paths,
416 flag=Bundle.FLAG_REAL_PATHS,
417 variables=job.variables,
418 default=False,
419 ):
420 Outputs.warning(
421 'The real paths feature is in conflict with the random paths feature...')
422 random_paths = True
424 # Prepare random paths
425 elif Flags.check_bool(
426 option=self.__options.random_paths,
427 flag=Bundle.FLAG_RANDOM_PATHS,
428 variables=job.variables,
429 default=False,
430 ):
431 random_paths = True
433 # Prepare real paths
434 elif Flags.check_bool(
435 option=self.__options.real_paths,
436 flag=Bundle.FLAG_REAL_PATHS,
437 variables=job.variables,
438 default=False,
439 ):
440 if Platform.IS_LINUX or Platform.IS_MAC_OS:
441 real_paths = True
443 # Unavailable feature
444 else: # pragma: no cover
445 Outputs.warning('The real paths feature is not available...')
447 # Initial job details
448 job_details_list: List[str] = []
449 job_details_string: str = ''
451 # Prepare when details
452 if job.when not in ['on_success']:
453 job_details_list += [f"when: {job.when}"]
455 # Prepare allow_failure details
456 if job.allow_failure:
457 job_details_list += ['failure allowed']
458 job_history.failure_allowed = True
460 # Prepare job details
461 if job_details_list:
462 job_details_string = f" ({', '.join(job_details_list)})"
464 # Update job details
465 job_history.details = job_details_string
467 # Filter when
468 if last_result and job.when not in [
469 'on_success',
470 'manual',
471 'always',
472 ]:
473 return last_result
474 if not last_result and job.when not in [
475 'on_failure',
476 'always',
477 ]:
478 return last_result
480 # Prepare image
481 image: str = job.image
483 # Prepare local runner
484 if self.__options.host or job.options.host:
485 image = 'local'
486 host = True
488 # Prepare quiet flag
489 quiet = Flags.check_bool(
490 option=self.__options.quiet,
491 flag=Bundle.FLAG_QUIET,
492 variables=job.variables,
493 default=False,
494 )
496 # Drop quiet flag
497 if not quiet and pipeline_history.jobs_quiet:
498 pipeline_history.jobs_quiet = False
500 # Prepare engine execution
501 if not host:
502 engines_names = Flags.check_str(
503 option=None,
504 flag=Bundle.FLAG_ENGINE,
505 variables=job.variables,
506 default=self.__options.engine,
507 )
508 if engines_names not in self.__engines or self.__engines[
509 engines_names] is None:
510 self.__engines[engines_names] = Engine(engines_names)
511 engine = self.__engines[engines_names]
512 assert engine is not None
513 engine_type = engine.name
515 # Prepare native execution
516 else:
517 engine_type = 'native'
519 # Header
520 if not quiet:
521 job_history.header(
522 pipeline_history.jobs_count,
523 image,
524 engine_type,
525 )
527 # Acquire project paths
528 path_project = Paths.resolve(self.__options.path)
529 path_parent = Paths.resolve(Path(self.__options.path).parent)
531 # Acquire project targets
532 if host or real_paths:
533 target_project = path_project
534 target_parent = path_parent
535 elif job.options.git_clone_path:
536 git_clone_path = PurePosixPath(job.options.git_clone_path)
537 target_project = Paths.get(git_clone_path / Path(path_project).name)
538 target_parent = Paths.get(git_clone_path)
539 elif random_paths:
540 random_path: str = Strings.random(8)
541 target_project = Paths.get(
542 Platform.BUILDS_DIR / random_path / Path(path_project).name)
543 target_parent = Paths.get(Platform.BUILDS_DIR / random_path)
544 else:
545 target_project = Paths.get(Platform.BUILDS_DIR / Path(path_project).name)
546 target_parent = Paths.get(Platform.BUILDS_DIR)
548 # Prepare specific working directory
549 if Flags.has(
550 option=self.__options.workdir,
551 flag=Bundle.FLAG_WORKDIR,
552 variables=job.variables,
553 ):
554 relativedir: Path = Path('.')
555 workdir: str = Flags.check_str(
556 option=self.__options.workdir,
557 flag=Bundle.FLAG_WORKDIR,
558 variables=job.variables,
559 default='.',
560 )
562 # Expand real working directory
563 if Platform.IS_LINUX or Platform.IS_MAC_OS:
564 workdir = Paths.expand(workdir)
565 if host or real_paths:
566 target_workdir = Paths.get((relativedir / workdir).resolve())
567 else:
568 target_workdir = Paths.get(PurePosixPath(target_project) / workdir)
570 # Expand remote working directory
571 else: # pragma: no cover
572 if workdir[0:1] == '~':
573 target_workdir = Paths.get(workdir)
574 else:
575 workdir = Paths.expand(workdir, home=False)
576 if host or real_paths:
577 target_workdir = Paths.get((relativedir / workdir).resolve())
578 else:
579 target_workdir = Paths.get(
580 PurePosixPath(target_project) / workdir)
582 # Prepare real working directory
583 elif host or real_paths:
584 target_workdir = Paths.get(self.__options.path)
586 # Prepare target working directory
587 else:
588 target_workdir = target_project
590 # Prepare entrypoint and scripts
591 scripts_after: List[str] = []
592 scripts_before: List[str] = []
593 scripts_commands: List[str] = []
594 scripts_debug: List[str] = []
596 # Prepare before_scripts
597 if Flags.check_bool(
598 option=self.__options.before,
599 flag=Bundle.FLAG_BEFORE,
600 variables=job.variables,
601 default=True,
602 ):
603 scripts_before += job.before_script
605 # Prepare scripts
606 scripts_commands += job.script
607 if not host:
608 if Flags.check_bool(
609 option=self.__options.bash,
610 flag=Bundle.FLAG_BASH,
611 variables=job.variables,
612 default=False,
613 ):
614 scripts_commands = []
615 if Flags.check_bool(
616 option=self.__options.bash,
617 flag=Bundle.FLAG_BASH,
618 variables=job.variables,
619 default=False,
620 ) or Flags.check_bool(
621 option=self.__options.debug,
622 flag=Bundle.FLAG_DEBUG,
623 variables=job.variables,
624 default=False,
625 ):
626 scripts_debug += [
627 f"echo \"{self.__MARKER_DEBUG}\"", 'tail -f /dev/null || true'
628 ]
630 # Prepare after_scripts
631 if Flags.check_bool(
632 option=self.__options.after,
633 flag=Bundle.FLAG_AFTER,
634 variables=job.variables,
635 default=True,
636 ):
637 scripts_after += job.after_script
639 # Prepare script file
640 script_file = ScriptsFile(
641 paths={
642 path_parent: target_parent,
643 path_project: target_project
644 },
645 prefix='.tmp.entrypoint.',
646 )
648 # Prepare execution context
649 script_file.shebang()
650 script_file.writelines([
651 '# Variables',
652 'result=1',
653 '',
654 ])
656 # Prepare host working directory
657 if host:
658 script_file.writelines([
659 '# Work directory',
660 f'cd "{target_workdir}"',
661 '',
662 ])
664 # Configure Git
665 if not host and not self.__options.no_git_safeties:
666 script_file.writelines([
667 '# Configure Git safeties',
668 'if type git >/dev/null 2>&1; then',
669 f' git config --global --add safe.directory "{target_workdir}"',
670 'else',
671 ' echo "[safe]" >>~/.gitconfig',
672 f' echo -e "\tdirectory = {target_workdir}" >>~/.gitconfig',
673 'fi',
674 '',
675 ])
677 # Configure Xauth, pylint: disable=line-too-long
678 if not host:
679 script_file.writelines([
680 '# Configure Xauthority',
681 f'if [ ! -z "${{{Bundle.ENV_DISPLAY_XAUTH_MAGIC_ADD}}}" ] && type xauth >/dev/null 2>&1; then',
682 f' if [ ! -z "${{{Platform.ENV_XAUTHORITY}}}" ] && [ ! -w "${{{Platform.ENV_XAUTHORITY}}}" ]; then',
683 ' true',
684 ' else',
685 f' xauth -q add ${{{Bundle.ENV_DISPLAY_XAUTH_MAGIC_ADD}}} 2>/dev/null',
686 ' fi',
687 'fi',
688 '',
689 ])
691 # Prepare before_script/script context
692 script_file.subshell_start('before_script/script')
693 script_file.configure(
694 errors=True,
695 verbose=job.options.verbose,
696 )
698 # Prepare before_script commands
699 if len(scripts_before) > 0:
700 script_file.write('# Run before_script')
701 script_file.subgroup_start()
702 script_file.writelines(scripts_before)
703 script_file.subgroup_stop()
704 script_file.write('')
706 # Prepare script commands
707 if len(scripts_commands) > 0:
708 script_file.write('# Run script')
709 script_file.subgroup_start()
710 script_file.writelines(scripts_commands)
711 script_file.subgroup_stop()
712 script_file.write('')
713 else:
714 script_file.write('# Missing script')
715 script_file.write('false')
716 script_file.write('')
718 # Finish before_script/script context
719 script_file.subshell_stop('before_script/script')
720 script_file.write('')
722 # Get before_script/script result
723 script_file.write('# Result')
724 script_file.write('result=${?}')
725 script_file.write('')
727 # Prepare container result
728 if not host:
729 script_file.writelines([
730 '# Result marker',
731 f'echo "{self.__MARKER_RESULT}:${{result}}"',
732 '',
733 ])
735 # Prepare debug script commands
736 if len(scripts_debug) > 0:
737 script_file.subshell_start('debug')
738 if job.options.verbose:
739 script_file.configure(
740 errors=False,
741 verbose=True,
742 )
743 script_file.writelines(scripts_debug)
744 script_file.subshell_stop('debug')
745 script_file.write('')
747 # Prepare after_script commands
748 if len(scripts_after) > 0:
749 script_file.subshell_start('after_script')
750 script_file.configure(
751 errors=True,
752 verbose=job.options.verbose,
753 )
754 script_file.write('# Run after_script')
755 script_file.subgroup_start()
756 script_file.writelines(scripts_after)
757 script_file.subgroup_stop()
758 script_file.subshell_stop('after_script')
759 script_file.write('')
761 # Prepare container ending
762 if not host:
763 script_file.writelines([
764 '# Ending marker',
765 f'echo "{self.__MARKER_ENDING}"',
766 '',
767 ])
769 # Prepare execution result
770 script_file.writelines([
771 '# Exit',
772 'exit "${result}"',
773 '',
774 ])
776 # Prepare script execution
777 script_stat = stat(script_file.name)
778 chmod(script_file.name, script_stat.st_mode | S_IXUSR | S_IXGRP
779 | S_IRGRP | S_IROTH | S_IXOTH)
780 script_file.close()
782 # Print script contents
783 if self.__options.scripts:
784 script_file.print()
785 return True
787 # Store environment
788 _environ = dict(environ)
790 # Acquire job data
791 env_builds_path: str = job.options.env_builds_path
792 env_job_name: str = job.options.env_job_name
793 env_job_name_slug: str = job.options.env_job_name_slug
794 env_job_path: str = job.options.env_job_path
795 variables = job.variables.copy()
797 # Configure job settings
798 environ[env_builds_path] = target_parent
799 environ[env_job_name] = job.name
800 environ[env_job_name_slug] = regex_sub(
801 r'[^a-z0-9]',
802 '-',
803 job.name.lower()[0:63],
804 ).strip('-')
805 environ[env_job_path] = target_project
806 environ[Bundle.ENV_LOCAL] = 'true'
807 environ[Bundle.ENV_PROJECT_NAME] = Paths.basename(target_project)
808 environ[Bundle.ENV_PROJECT_NAMESPACE] = Paths.basename(target_parent)
810 # Prepare Git environment
811 git: Git = Git()
813 # Configure Git variables
814 if Bundle.ENV_COMMIT_REF_NAME in variables:
815 environ[Bundle.ENV_COMMIT_REF_NAME] = variables[Bundle.ENV_COMMIT_REF_NAME]
816 else:
817 environ[Bundle.ENV_COMMIT_REF_NAME] = git.head_reference_name(path_project)
818 if Bundle.ENV_COMMIT_REF_SLUG in variables:
819 environ[Bundle.ENV_COMMIT_REF_SLUG] = variables[Bundle.ENV_COMMIT_REF_SLUG]
820 else:
821 environ[Bundle.ENV_COMMIT_REF_SLUG] = git.head_reference_slug(
822 path_project, name=environ[Bundle.ENV_COMMIT_REF_NAME])
823 if Bundle.ENV_COMMIT_SHA in variables:
824 environ[Bundle.ENV_COMMIT_SHA] = variables[Bundle.ENV_COMMIT_SHA]
825 else:
826 environ[Bundle.ENV_COMMIT_SHA] = git.head_revision_hash(path_project)
827 if Bundle.ENV_COMMIT_SHORT_SHA1 in variables:
828 environ[Bundle.ENV_COMMIT_SHORT_SHA1] = variables[
829 Bundle.ENV_COMMIT_SHORT_SHA1]
830 else:
831 environ[Bundle.ENV_COMMIT_SHORT_SHA1] = git.head_revision_short_hash(
832 path_project)
833 if Bundle.ENV_COMMIT_TIMESTAMP in variables:
834 environ[Bundle.ENV_COMMIT_TIMESTAMP] = variables[Bundle.ENV_COMMIT_TIMESTAMP]
835 else:
836 environ[Bundle.ENV_COMMIT_TIMESTAMP] = git.head_revision_timestamp(
837 path_project)
838 if Bundle.ENV_DEFAULT_BRANCH in variables:
839 environ[Bundle.ENV_DEFAULT_BRANCH] = variables[Bundle.ENV_DEFAULT_BRANCH]
840 else:
841 environ[Bundle.ENV_DEFAULT_BRANCH] = git.branch_default(path_project)
843 # Prepare network
844 network = ''
845 if self.__options.network:
846 network = self.__options.network
847 variables[Bundle.FLAG_NETWORK] = network
848 elif Bundle.FLAG_NETWORK in variables:
849 network = variables[Bundle.FLAG_NETWORK]
850 variables[Bundle.FLAG_NETWORK] = network
852 # Prepare privileged
853 privileged = Flags.check_bool(
854 option=self.__options.privileged,
855 flag=Bundle.FLAG_PRIVILEGED,
856 variables=variables,
857 default=privileged_default(),
858 )
859 variables[Bundle.FLAG_PRIVILEGED] = Flags.string(privileged)
861 # Prepare user variables
862 environ[Bundle.ENV_USER_HOST_GID] = str(Platform.getgid())
863 environ[Bundle.ENV_USER_HOST_UID] = str(Platform.getuid())
864 environ[Bundle.ENV_USER_HOST_USERNAME] = Platform.getusername()
866 # Prepare CI variables
867 for variable in [
868 env_builds_path,
869 env_job_name,
870 env_job_name_slug,
871 env_job_path,
872 Bundle.ENV_COMMIT_REF_NAME,
873 Bundle.ENV_COMMIT_REF_SLUG,
874 Bundle.ENV_COMMIT_SHA,
875 Bundle.ENV_COMMIT_SHORT_SHA1,
876 Bundle.ENV_COMMIT_TIMESTAMP,
877 Bundle.ENV_DEFAULT_BRANCH,
878 Bundle.ENV_LOCAL,
879 Bundle.ENV_PROJECT_NAME,
880 Bundle.ENV_PROJECT_NAMESPACE,
881 Bundle.ENV_USER_HOST_GID,
882 Bundle.ENV_USER_HOST_UID,
883 Bundle.ENV_USER_HOST_USERNAME,
884 ]:
885 if variable not in variables:
886 variables[variable] = environ[variable]
888 # Prepare job variables
889 for variable in variables:
890 variables[variable] = Environment.expand(variables[variable])
892 # Prepare environment
893 environ.update(variables)
895 # Prepare job variables
896 for variable in variables:
897 variables[variable] = Environment.expand(
898 variables[variable],
899 variable=variable,
900 unknowns=True,
901 )
903 # Restore environment
904 environ.clear()
905 environ.update(_environ)
907 # Container execution
908 if not host:
909 result = self.__run_container(
910 engine=engine,
911 variables=variables,
912 path_parent=path_parent,
913 target_parent=target_parent,
914 image=image,
915 job=job,
916 script_file=script_file,
917 entrypoint=job.entrypoint,
918 network=network,
919 privileged=privileged,
920 target_workdir=target_workdir,
921 last_result=last_result,
922 result=result,
923 )
925 # Native execution
926 else:
927 result = self.__run_native(
928 variables=variables,
929 entrypoint=job.entrypoint,
930 script_file=script_file,
931 job=job,
932 last_result=last_result,
933 result=result,
934 )
936 # Update job history
937 job_history.result = result
939 # Update interacted flag
940 if not pipeline_history.interacted and self.__interrupted:
941 pipeline_history.interacted = True
943 # Update interrupted flag
944 job_history.interrupted = self.__interrupted
946 # Separator
947 print(' ')
948 Platform.flush()
950 # Footer
951 if not quiet:
952 job_history.footer()
954 # Allowed failure result
955 if job.when not in [
956 'on_failure',
957 'always',
958 ] and not result and job.allow_failure:
959 result = True
961 # Result
962 return result