@@ -332,6 +332,144 @@ def build(ctx, dist: str, variant: str, tag: Optional[str],
332332 sys .exit (130 )
333333
334334
335+ @main .command ()
336+ @click .argument ('dist' , required = True )
337+ @click .option ('--variant' , default = 'feelpp-env' , help = 'Variant to push' )
338+ @click .option ('--registry' , default = 'ghcr.io' , help = 'Container registry' )
339+ @click .option ('--repo' , default = 'feelpp' , help = 'Repository organization' )
340+ @click .option ('--token' , help = 'GitHub token (default: $CR_PAT or $GITHUB_TOKEN)' )
341+ @click .option ('--user' , help = 'Registry username (default: $GITHUB_ACTOR)' )
342+ @click .option ('--dry-run' , is_flag = True , help = 'Show what would be pushed without pushing' )
343+ @click .pass_context
344+ def push (ctx , dist : str , variant : str , registry : str , repo : str ,
345+ token : Optional [str ], user : Optional [str ], dry_run : bool ):
346+ """Push Docker image to container registry (ghcr.io).
347+
348+ DIST should be in format 'name:version' (e.g., ubuntu:24.04, debian:13)
349+
350+ Requires GitHub Personal Access Token with packages:write scope.
351+ Set via CR_PAT or GITHUB_TOKEN environment variable, or use --token option.
352+
353+ Examples:
354+
355+ # Push Ubuntu 24.04 (using CR_PAT env var)
356+ export CR_PAT=ghp_xxxxxxxxxxxx
357+ ./factory.sh push ubuntu:24.04
358+
359+ # Push with explicit token
360+ ./factory.sh push debian:13 --token ghp_xxxxx
361+
362+ # Dry run to preview
363+ ./factory.sh push ubuntu:24.04 --dry-run
364+
365+ # Push to different registry
366+ ./factory.sh push debian:sid --registry docker.io --repo myuser
367+ """
368+ import os
369+
370+ # Parse distribution
371+ try :
372+ dist_name , version_tag = dist .split (':' )
373+ except ValueError :
374+ console .print (f"[red]Error:[/red] DIST must be in format 'name:version' (got '{ dist } ')" )
375+ console .print ("Examples: ubuntu:24.04, debian:13, fedora:42" )
376+ sys .exit (1 )
377+
378+ # Get token from environment if not provided
379+ if not token :
380+ token = os .getenv ('CR_PAT' ) or os .getenv ('GITHUB_TOKEN' )
381+ if not token :
382+ console .print ("[red]Error:[/red] GitHub token required" )
383+ console .print ("Set CR_PAT or GITHUB_TOKEN environment variable, or use --token option" )
384+ sys .exit (1 )
385+
386+ # Get username from environment if not provided
387+ if not user :
388+ user = os .getenv ('GITHUB_ACTOR' ) or os .getenv ('USER' )
389+ if not user :
390+ console .print ("[red]Error:[/red] Username required" )
391+ console .print ("Set GITHUB_ACTOR environment variable, or use --user option" )
392+ sys .exit (1 )
393+
394+ # Construct tags
395+ local_tag = f"{ variant } :{ dist_name } -{ version_tag } "
396+ remote_tag = f"{ registry } /{ repo } /{ variant } :{ dist_name } -{ version_tag } "
397+
398+ console .print ("\n [green]═══ Pushing to Container Registry ═══[/green]" )
399+ console .print (f"Distribution: { dist } " )
400+ console .print (f"Local tag: { local_tag } " )
401+ console .print (f"Remote tag: { remote_tag } " )
402+ console .print (f"Registry: { registry } " )
403+ console .print (f"Repository: { repo } /{ variant } " )
404+ console .print ()
405+
406+ if dry_run :
407+ console .print ("[yellow]DRY RUN - No actual push will occur[/yellow]\n " )
408+ console .print ("Would execute:" )
409+ console .print (f" echo <token> | docker login { registry } -u { user } --password-stdin" )
410+ console .print (f" docker tag { local_tag } { remote_tag } " )
411+ console .print (f" docker push { remote_tag } " )
412+ return
413+
414+ # Check if local image exists
415+ try :
416+ result = subprocess .run (
417+ ['docker' , 'image' , 'inspect' , local_tag ],
418+ capture_output = True ,
419+ check = False
420+ )
421+ if result .returncode != 0 :
422+ console .print (f"[red]Error:[/red] Local image '{ local_tag } ' not found\n " )
423+ console .print ("Build it first with:" )
424+ console .print (f" ./factory.sh build { dist } " )
425+ sys .exit (1 )
426+ except FileNotFoundError :
427+ console .print ("[red]Error:[/red] Docker command not found. Is Docker installed?" )
428+ sys .exit (1 )
429+
430+ # Login to registry
431+ console .print (f"[blue]Logging in to { registry } ...[/blue]" )
432+ try :
433+ result = subprocess .run (
434+ ['docker' , 'login' , registry , '-u' , user , '--password-stdin' ],
435+ input = token .encode (),
436+ capture_output = True ,
437+ check = False
438+ )
439+ if result .returncode != 0 :
440+ console .print (f"[red]Login failed:[/red] { result .stderr .decode ()} " )
441+ sys .exit (1 )
442+ except Exception as e :
443+ console .print (f"[red]Login error:[/red] { e } " )
444+ sys .exit (1 )
445+
446+ # Tag image
447+ console .print ("[blue]Tagging image...[/blue]" )
448+ result = subprocess .run (
449+ ['docker' , 'tag' , local_tag , remote_tag ],
450+ capture_output = True ,
451+ check = False
452+ )
453+ if result .returncode != 0 :
454+ console .print (f"[red]Tag failed:[/red] { result .stderr .decode ()} " )
455+ sys .exit (1 )
456+
457+ # Push image
458+ console .print ("[blue]Pushing image...[/blue]" )
459+ result = subprocess .run (
460+ ['docker' , 'push' , remote_tag ],
461+ check = False
462+ )
463+
464+ if result .returncode == 0 :
465+ console .print (f"\n [green]✓ Successfully pushed { remote_tag } [/green]\n " )
466+ console .print ("Pull with:" )
467+ console .print (f" docker pull { remote_tag } " )
468+ else :
469+ console .print (f"\n [red]✗ Push failed with exit code { result .returncode } [/red]" )
470+ sys .exit (result .returncode )
471+
472+
335473@main .command ()
336474@click .option ('--variant' , default = 'feelpp-env' , help = 'Variant to update CI for' )
337475@click .option ('--workflow-file' , type = click .Path (path_type = Path ),
0 commit comments