Coverage for src/cli/main.py: 100.00%
116 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 (
5 _ArgumentGroup,
6 _MutuallyExclusiveGroup,
7 ArgumentParser,
8 Namespace,
9 RawTextHelpFormatter,
10 SUPPRESS,
11)
12from os import environ
13from shutil import get_terminal_size
14from sys import exit as sys_exit
16# Components
17from ..engines.engine import supported as engine_supported, privileged_default
18from ..package.bundle import Bundle
19from ..package.settings import Settings
20from ..package.updates import Updates
21from ..package.version import Version
22from ..prints.colors import Colors
23from ..system.platform import Platform
24from .entrypoint import Entrypoint
26# Constants
27HELP_POSITION: int = 23
29# Main, pylint: disable=too-many-branches,too-many-statements
30def main() -> None:
32 # Variables
33 group: _ArgumentGroup
34 result: Entrypoint.Result
35 subgroup: _MutuallyExclusiveGroup
37 # Arguments creation
38 parser: ArgumentParser = ArgumentParser(
39 prog=Bundle.NAME,
40 description=f'{Bundle.NAME}: {Bundle.DESCRIPTION}',
41 add_help=False,
42 formatter_class=lambda prog: RawTextHelpFormatter(
43 prog,
44 max_help_position=HELP_POSITION,
45 width=min(
46 120,
47 get_terminal_size().columns - 2,
48 ),
49 ),
50 )
52 # Arguments internal definitions
53 group = parser.add_argument_group('internal arguments')
54 group.add_argument(
55 '-h',
56 '--help',
57 dest='help',
58 action='store_true',
59 help='Show this help message',
60 )
61 group.add_argument(
62 '--version',
63 dest='version',
64 action='store_true',
65 help='Show the current version',
66 )
67 group.add_argument(
68 '--no-color',
69 dest='no_color',
70 action='store_true',
71 help=f'Disable colors outputs with \'{Bundle.ENV_NO_COLOR}=1\'\n'
72 '(or default settings: [themes] > no_color)',
73 )
74 group.add_argument(
75 '--update-check',
76 dest='update_check',
77 action='store_true',
78 help='Check for newer package updates',
79 )
80 group.add_argument(
81 '--settings',
82 dest='settings',
83 action='store_true',
84 help='Show the current settings path and contents',
85 )
86 group.add_argument(
87 '--set',
88 dest='set',
89 action='store',
90 metavar=('GROUP', 'KEY', 'VAL'),
91 nargs=3,
92 help='Set settings specific \'VAL\' value to [GROUP] > KEY\n' \
93 'or unset by using \'UNSET\' as \'VAL\'',
94 )
96 # Arguments pipeline definitions
97 group = parser.add_argument_group('pipeline arguments')
98 group.add_argument(
99 '-p',
100 '--pipeline',
101 dest='pipeline',
102 action='store_true',
103 help='Automatically run pipeline stages rather than jobs',
104 )
105 group.add_argument(
106 '-q',
107 '--quiet',
108 dest='quiet',
109 action='store_true',
110 help='Hide jobs execution context',
111 )
112 group.add_argument(
113 '-c',
114 dest='configuration',
115 action='store',
116 default=Bundle.CONFIGURATION,
117 help=f'Path to the {Bundle.CONFIGURATION} configuration file or folder',
118 )
119 group.add_argument(
120 '-B',
121 '--no-before',
122 dest='before',
123 action='store_false',
124 help='Disable before_script executions',
125 )
126 group.add_argument(
127 '-A',
128 '--no-after',
129 dest='after',
130 action='store_false',
131 help='Disable after_script executions',
132 )
133 group.add_argument(
134 '-C',
135 dest='commands',
136 action='store',
137 help='Run specific commands instead of "scripts" commands',
138 )
139 group.add_argument(
140 '-n', dest='network', action='store', help=
141 f'Configure the network mode used (or define {Bundle.FLAG_NETWORK})\n' \
142 f"Choices: {', '.join(Bundle.ARGUMENT_NETWORKS_ENUM)}. " \
143 f'Default: {Bundle.ARGUMENT_NETWORKS_ENUM[0]}'
144 )
145 group.add_argument(
146 '-e',
147 dest='env',
148 action='append',
149 help='Define VARIABLE=value, pass VARIABLE or ENV file',
150 )
151 group.add_argument(
152 '-E', dest='engine', action='store', help=
153 f'Force a specific engine (or define {Bundle.FLAG_ENGINE})\n' \
154 f"Default list: {','.join(engine_supported())}"
155 )
156 group.add_argument(
157 '-H',
158 '--host',
159 dest='host',
160 action='store_true',
161 help=f'Run jobs on the host rather than containers (or define {Bundle.FLAG_HOST})',
162 )
163 group.add_argument(
164 '--notify',
165 dest='notify',
166 action='store_true',
167 help='Enable host notifications of pipeline and jobs results',
168 )
169 group.add_argument(
170 '--privileged',
171 dest='privileged',
172 action='store',
173 metavar='BOOL',
174 nargs='?',
175 const='true' if privileged_default() else 'false',
176 help='Give extended privileges to the containers\n'
177 f'(Defaults to \'%(const)s\', or define {Bundle.FLAG_PRIVILEGED})',
178 )
179 group.add_argument(
180 '--random-paths',
181 dest='random_paths',
182 action='store_true',
183 help='Mount random folder paths in the container',
184 )
185 group.add_argument(
186 '-r',
187 '--real-paths',
188 dest='real_paths',
189 action='store_true',
190 help='Mount real folder paths in the container (Linux / macOS only)',
191 )
192 group.add_argument(
193 '-S',
194 '--sockets',
195 dest='sockets',
196 action='store_true',
197 help='Mount engine sockets for nested containers\n'
198 '(Enabled by default with services: docker:*dind)',
199 )
200 group.add_argument(
201 '--ssh',
202 dest='ssh',
203 action='store',
204 metavar='SSH_USER',
205 nargs='?',
206 const=Bundle.ARGUMENT_SSH_USER_DEFAULT,
207 help='Bind SSH credentials to a container\'s user',
208 )
209 group.add_argument(
210 '-v',
211 dest='volume',
212 action='append',
213 help='Mount VOLUME or HOST:TARGET in containers',
214 )
215 group.add_argument(
216 '-w',
217 dest='workdir',
218 action='store',
219 help='Override the container\'s working path',
220 )
222 # Arguments debugging definitions
223 group = parser.add_argument_group('debugging arguments')
224 subgroup = group.add_mutually_exclusive_group()
225 subgroup.add_argument(
226 '--bash',
227 dest='bash',
228 action='store_true',
229 help='Prepare runners for manual bash purposes',
230 )
231 subgroup.add_argument(
232 '--debug',
233 dest='debug',
234 action='store_true',
235 help='Keep runners active for debugging purposes',
236 )
237 group.add_argument(
238 '--display',
239 dest='display',
240 action='store_true',
241 help='Enable host DISPLAY forwarding features',
242 )
243 group.add_argument(
244 '--shell',
245 dest='shell',
246 action='store',
247 help='Configure the default bash/debug shell entrypoint',
248 )
250 # Arguments jobs definitions
251 group = parser.add_argument_group('jobs arguments')
252 group.add_argument(
253 '--all',
254 dest='all',
255 action='store_true',
256 help='Enable all jobs by default in selections',
257 )
258 group.add_argument(
259 '--configure',
260 dest='configure',
261 action='store_true',
262 help='Show interactive configurations for CI/CD inputs',
263 )
264 subgroup = group.add_mutually_exclusive_group()
265 subgroup.add_argument(
266 '--input',
267 dest='input',
268 action='append',
269 metavar='KEY=VAL',
270 help='Define KEY=value for CI/CD inputs',
271 )
272 group.add_argument(
273 '-f',
274 '--force',
275 dest='force',
276 action='store_true',
277 help='Force the action (use with --pull)',
278 )
279 group.add_argument(
280 '-i',
281 '--ignore-case',
282 dest='ignore_case',
283 action='store_true',
284 help='Ignore case when searching for names',
285 )
286 group.add_argument(
287 '-m',
288 '--manual',
289 dest='manual',
290 action='store_true',
291 help='Allow manual jobs to be used',
292 )
293 group.add_argument(
294 '--no-console',
295 dest='no_console',
296 action='store_true',
297 help='Disable console launch in bash/debug modes\n'
298 '(or default settings: [runner] > no_console)',
299 )
300 group.add_argument(
301 '--no-git-safeties',
302 dest='no_git_safeties',
303 action='store_true',
304 help='Disable automated Git safeties configuration\n'
305 '(or default settings: [runner] > no_git_safeties)',
306 )
307 group.add_argument(
308 '--no-script-fail',
309 dest='no_script_fail',
310 action='store_true',
311 help='Fail on missing \'script\' nodes of jobs\n'
312 '(or default settings: [runner] > no_script_fail)',
313 )
314 group.add_argument(
315 '-R',
316 '--no-regex',
317 dest='no_regex',
318 action='store_true',
319 help='Disable regex search of names',
320 )
321 group.add_argument(
322 '--no-verbose',
323 dest='no_verbose',
324 action='store_true',
325 help='Hide jobs verbose outputs',
326 )
327 group.add_argument(
328 '--scripts',
329 dest='scripts',
330 action='store_true',
331 help='Dump parsed jobs entrypoint scripts',
332 )
333 group.add_argument(
334 '-t', dest='tags', action='store', help=
335 f"Handle listed tags as manual jobs\nDefault list: {','.join(Bundle.ARGUMENT_TAGS_DEFAULT)}"
336 )
338 # Arguments features definitions
339 group = parser.add_argument_group('features arguments')
340 subgroup = group.add_mutually_exclusive_group()
341 subgroup.add_argument(
342 '-d',
343 '--dump',
344 dest='dump',
345 action='store_true',
346 help=f'Dump parsed {Bundle.CONFIGURATION} configuration',
347 )
348 subgroup.add_argument(
349 '-s',
350 '--select',
351 dest='select',
352 action='store_true',
353 help='Force jobs selection from enumerated names',
354 )
355 subgroup.add_argument(
356 '-P',
357 '--presets',
358 dest='presets',
359 action='store_true',
360 help='Select jobs with preset names',
361 )
362 subgroup.add_argument(
363 '-l',
364 '--list',
365 dest='list',
366 action='store_true',
367 help='Select one job to run (implies --manual)',
368 )
369 subgroup.add_argument(
370 '--pull',
371 dest='pull',
372 action='store_true',
373 help='Pull container images from jobs',
374 )
375 subgroup.add_argument(
376 '--rmi',
377 dest='rmi',
378 action='store_true',
379 help='Delete container images from jobs',
380 )
382 # Arguments hidden definitions
383 group = parser.add_argument_group('hidden arguments')
384 group.add_argument(
385 '--engine-default',
386 dest='engine_default',
387 action='store_true',
388 help=SUPPRESS,
389 )
390 group.add_argument(
391 '--image',
392 dest='image',
393 action='store',
394 help=SUPPRESS,
395 )
396 group.add_argument(
397 '--tags-default',
398 dest='tags_default',
399 action='store_true',
400 help=SUPPRESS,
401 )
403 # Arguments positional definitions
404 group = parser.add_argument_group('positional arguments')
405 group.add_argument(
406 '--',
407 dest='double_dash',
408 action='store_true',
409 help='Positional arguments separator (recommended)',
410 )
411 group.add_argument(
412 'names',
413 nargs='*',
414 help='Names of specific jobs (or stages with --pipeline)\n'
415 'Regex names is supported unless --no-regex is used',
416 )
418 # Arguments parser
419 options: Namespace = parser.parse_args()
421 # Help informations
422 if options.help:
423 print(' ')
424 parser.print_help()
425 print(' ')
426 Platform.flush()
427 sys_exit(0)
429 # Instantiate settings
430 settings: Settings = Settings(name=Bundle.NAME)
432 # Prepare no_color
433 if not options.no_color:
434 if settings.has('themes', 'no_color'):
435 options.no_color = settings.get_bool('themes', 'no_color')
436 else:
437 options.no_color = False
438 settings.set_bool('themes', 'no_color', options.no_color)
440 # Configure no_color
441 if options.no_color:
442 environ[Bundle.ENV_FORCE_COLOR] = '0'
443 environ[Bundle.ENV_NO_COLOR] = '1'
445 # Prepare colors
446 Colors.prepare()
448 # Settings setter
449 if options.set:
450 settings.set(options.set[0], options.set[1], options.set[2])
451 settings.show()
452 sys_exit(0)
454 # Settings informations
455 if options.settings:
456 settings.show()
457 sys_exit(0)
459 # Instantiate updates
460 updates: Updates = Updates(
461 name=Bundle.PACKAGE,
462 settings=settings,
463 )
465 # Version informations
466 if options.version:
467 print(
468 f'{Bundle.NAME} {Version.get()} from {Version.path()} (python {Version.python()})'
469 )
470 Platform.flush()
471 sys_exit(0)
473 # Check for current updates
474 if options.update_check:
475 if not updates.check():
476 updates.check(older=True)
477 sys_exit(0)
479 # CLI entrypoint
480 result = Entrypoint.cli(
481 options,
482 settings,
483 )
485 # Check for daily updates
486 if updates.enabled and updates.daily:
487 updates.check()
489 # Result
490 if result in [
491 Entrypoint.Result.SUCCESS,
492 Entrypoint.Result.FINALIZE,
493 ]:
494 sys_exit(0)
495 else:
496 sys_exit(1)
498# Entrypoint
499if __name__ == '__main__': # pragma: no cover
500 main()