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

1#!/usr/bin/env python3 

2 

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 

15 

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 

25 

26# Constants 

27HELP_POSITION: int = 23 

28 

29# Main, pylint: disable=too-many-branches,too-many-statements 

30def main() -> None: 

31 

32 # Variables 

33 group: _ArgumentGroup 

34 result: Entrypoint.Result 

35 subgroup: _MutuallyExclusiveGroup 

36 

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 ) 

51 

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 ) 

95 

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 ) 

221 

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 ) 

249 

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 ) 

337 

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 ) 

381 

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 ) 

402 

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 ) 

417 

418 # Arguments parser 

419 options: Namespace = parser.parse_args() 

420 

421 # Help informations 

422 if options.help: 

423 print(' ') 

424 parser.print_help() 

425 print(' ') 

426 Platform.flush() 

427 sys_exit(0) 

428 

429 # Instantiate settings 

430 settings: Settings = Settings(name=Bundle.NAME) 

431 

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) 

439 

440 # Configure no_color 

441 if options.no_color: 

442 environ[Bundle.ENV_FORCE_COLOR] = '0' 

443 environ[Bundle.ENV_NO_COLOR] = '1' 

444 

445 # Prepare colors 

446 Colors.prepare() 

447 

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) 

453 

454 # Settings informations 

455 if options.settings: 

456 settings.show() 

457 sys_exit(0) 

458 

459 # Instantiate updates 

460 updates: Updates = Updates( 

461 name=Bundle.PACKAGE, 

462 settings=settings, 

463 ) 

464 

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) 

472 

473 # Check for current updates 

474 if options.update_check: 

475 if not updates.check(): 

476 updates.check(older=True) 

477 sys_exit(0) 

478 

479 # CLI entrypoint 

480 result = Entrypoint.cli( 

481 options, 

482 settings, 

483 ) 

484 

485 # Check for daily updates 

486 if updates.enabled and updates.daily: 

487 updates.check() 

488 

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) 

497 

498# Entrypoint 

499if __name__ == '__main__': # pragma: no cover 

500 main()