Skip to content

Commit 404f4ce

Browse files
committed
Added print state tables
1 parent a60fbcf commit 404f4ce

File tree

2 files changed

+211
-1
lines changed

2 files changed

+211
-1
lines changed

src/redis_release/bht/state.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
import logging
33
import uuid
44
from datetime import datetime
5+
from importlib.metadata import packages_distributions
56
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Union
7+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Union
78

89
from botocore.exceptions import ClientError
910
from pydantic import BaseModel, Field
11+
from rich.console import Console
1012
from rich.pretty import pretty_repr
13+
from rich.table import Table
1114

1215
from redis_release.models import WorkflowConclusion, WorkflowStatus
1316
from redis_release.state_manager import S3Backed, logger
@@ -238,6 +241,7 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
238241
self.storage.release_lock(self.tag)
239242
self._lock_acquired = False
240243
logger.info(f"Lock released for tag: {self.tag}")
244+
print_state_table(self.state)
241245

242246
@property
243247
def state(self) -> ReleaseState:
@@ -508,3 +512,181 @@ def reset_model_to_defaults(target: BaseModel, default: BaseModel) -> None:
508512
else:
509513
# Simple value, copy directly
510514
setattr(target, field_name, default_value)
515+
516+
517+
def print_state_table(state: ReleaseState, console: Optional[Console] = None) -> None:
518+
"""Print table showing the release state.
519+
520+
Args:
521+
state: The ReleaseState to display
522+
console: Optional Rich Console instance (creates new one if not provided)
523+
"""
524+
if console is None:
525+
console = Console()
526+
527+
# Create table with title
528+
table = Table(
529+
title=f"[bold cyan]Release State: {state.meta.tag or 'N/A'}[/bold cyan]",
530+
show_header=True,
531+
header_style="bold magenta",
532+
border_style="bright_blue",
533+
title_style="bold cyan",
534+
)
535+
536+
# Add columns
537+
table.add_column("Package", style="cyan", no_wrap=True, width=20)
538+
table.add_column("Build", justify="center", width=15)
539+
table.add_column("Publish", justify="center", width=15)
540+
table.add_column("Details", style="yellow", width=40)
541+
542+
# Process each package
543+
for package_name, package in sorted(state.packages.items()):
544+
# Determine build status
545+
build_status = _get_workflow_status_display(package.build)
546+
547+
# Determine publish status
548+
publish_status = _get_workflow_status_display(package.publish)
549+
550+
# Collect details from workflows
551+
details = _collect_details(package)
552+
553+
# Add row to table
554+
table.add_row(
555+
package_name,
556+
build_status,
557+
publish_status,
558+
details,
559+
)
560+
561+
# Print the table
562+
console.print()
563+
console.print(table)
564+
console.print()
565+
566+
567+
def _get_workflow_status_display(workflow: Workflow) -> str:
568+
"""Get a rich-formatted status display for a workflow.
569+
570+
Args:
571+
workflow: The workflow to check
572+
573+
Returns:
574+
Rich-formatted status string
575+
"""
576+
# Check result field - if we have result, we succeeded
577+
if workflow.result is not None:
578+
return "[bold green]✓ Success[/bold green]"
579+
580+
# Check if workflow was triggered
581+
if workflow.triggered_at is None:
582+
return "[dim]− Not Started[/dim]"
583+
584+
# Workflow was triggered but no result - it failed
585+
return "[bold red]✗ Failed[/bold red]"
586+
587+
588+
def _collect_workflow_details(workflow: Workflow, prefix: str) -> List[str]:
589+
"""Collect details from a workflow using bottom-up approach.
590+
591+
Shows successes until the first failure, then stops.
592+
Bottom-up means: trigger → identify → timeout → conclusion → artifacts → result
593+
594+
Args:
595+
workflow: The workflow to check
596+
prefix: Prefix for detail messages (e.g., "Build" or "Publish")
597+
598+
Returns:
599+
List of detail strings
600+
"""
601+
details: List[str] = []
602+
603+
# Stage 1: Trigger (earliest/bottom)
604+
if workflow.ephemeral.trigger_failed or workflow.triggered_at is None:
605+
details.append(f"[red]✗ Trigger {prefix} workflow failed[/red]")
606+
return details
607+
else:
608+
details.append(f"[green]✓ {prefix} workflow triggered[/green]")
609+
610+
# Stage 2: Identify
611+
if workflow.ephemeral.identify_failed or workflow.run_id is None:
612+
details.append(f"[red]✗ {prefix} workflow not found[/red]")
613+
return details
614+
else:
615+
details.append(f"[green]✓ {prefix} workflow found[/green]")
616+
617+
# Stage 3: Timeout (only ephemeral)
618+
if workflow.ephemeral.timed_out:
619+
details.append(f"[yellow]⏱ {prefix} timed out[/yellow]")
620+
return details
621+
622+
# Stage 4: Workflow conclusion
623+
if workflow.conclusion == WorkflowConclusion.FAILURE:
624+
details.append(f"[red]✗ {prefix} workflow failed[/red]")
625+
return details
626+
627+
# Stage 5: Artifacts download
628+
if workflow.ephemeral.artifacts_download_failed or workflow.artifacts is None:
629+
details.append(f"[red]✗ {prefix} artifacts download failed[/red]")
630+
return details
631+
else:
632+
details.append(f"[green]✓ {prefix} artifacts downloaded[/green]")
633+
634+
# Stage 6: Result extraction (latest/top)
635+
if workflow.result is None or workflow.ephemeral.extract_result_failed:
636+
details.append(f"[red]✗ {prefix} failed to extract result[/red]")
637+
return details
638+
else:
639+
details.append(f"[green]✓ {prefix} result extracted[/green]")
640+
641+
# Check for other workflow states
642+
if workflow.status == WorkflowStatus.IN_PROGRESS:
643+
details.append(f"[blue]⟳ {prefix} in progress[/blue]")
644+
elif workflow.status == WorkflowStatus.QUEUED:
645+
details.append(f"[cyan]⋯ {prefix} queued[/cyan]")
646+
elif workflow.status == WorkflowStatus.PENDING:
647+
details.append(f"[dim]○ {prefix} pending[/dim]")
648+
649+
return details
650+
651+
652+
def _collect_package_details(package: Package) -> List[str]:
653+
"""Collect details from package metadata.
654+
655+
Args:
656+
package: The package to check
657+
658+
Returns:
659+
List of detail strings (may be empty)
660+
"""
661+
details: List[str] = []
662+
663+
if package.meta.ephemeral.identify_ref_failed:
664+
details.append("[red]✗ Identify target ref to run workflow failed[/red]")
665+
elif package.meta.ref is not None:
666+
details.append(f"[green]✓ Target Ref identified: {package.meta.ref}[/green]")
667+
668+
return details
669+
670+
671+
def _collect_details(package: Package) -> str:
672+
"""Collect and format all details from package and workflows.
673+
674+
Args:
675+
package: The package to check
676+
677+
Returns:
678+
Formatted string of details
679+
"""
680+
details: List[str] = []
681+
682+
# Collect package-level details
683+
details.extend(_collect_package_details(package))
684+
685+
# Collect build workflow details
686+
details.extend(_collect_workflow_details(package.build, "Build"))
687+
688+
# Only collect publish details if build succeeded (has result)
689+
if package.build.result is not None:
690+
details.extend(_collect_workflow_details(package.publish, "Publish"))
691+
692+
return "\n".join(details)

src/redis_release/cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
PackageMeta,
1818
ReleaseMeta,
1919
ReleaseState,
20+
S3StateStorage,
21+
StateSyncer,
2022
Workflow,
2123
)
2224

@@ -345,5 +347,31 @@ def release_bht(
345347
asyncio.run(async_tick_tock(tree))
346348

347349

350+
@app.command()
351+
def release_state(
352+
release_tag: str = typer.Argument(..., help="Release tag (e.g., 8.4-m01-int1)"),
353+
config_file: Optional[str] = typer.Option(
354+
None, "--config", "-c", help="Path to config file (default: config.yaml)"
355+
),
356+
) -> None:
357+
"""Run release using behaviour tree implementation."""
358+
setup_logging(logging.INFO)
359+
config_path = config_file or "config.yaml"
360+
config = load_config(config_path)
361+
362+
# Create release args
363+
args = ReleaseArgs(
364+
release_tag=release_tag,
365+
force_rebuild=[],
366+
)
367+
368+
with StateSyncer(
369+
storage=S3StateStorage(),
370+
config=config,
371+
args=args,
372+
):
373+
pass
374+
375+
348376
if __name__ == "__main__":
349377
app()

0 commit comments

Comments
 (0)