Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/AllGitStatus/MainApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class Column:
PullRequestsColumn = Column(9, "PRs", "center")
SecurityAlertsColumn = Column(10, "Security", "center")
CICDStatusColumn = Column(11, "CI/CD", "center")
ArchivedColumn = Column(12, "Archived", "center")
ReleaseColumn = Column(12, "Release", "center")
ArchivedColumn = Column(13, "Archived", "center")

COLUMN_MAP: dict[
tuple[
Expand All @@ -66,6 +67,7 @@ class Column:
(GitHubSource.__name__, "pull_requests"): PullRequestsColumn,
(GitHubSource.__name__, "security_alerts"): SecurityAlertsColumn,
(GitHubSource.__name__, "cicd_status"): CICDStatusColumn,
(GitHubSource.__name__, "release"): ReleaseColumn,
(GitHubSource.__name__, "archived"): ArchivedColumn,
}

Expand Down
105 changes: 105 additions & 0 deletions src/AllGitStatus/Sources/GitHubSource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, UTC
from http import HTTPStatus

import aiohttp

Expand Down Expand Up @@ -62,6 +63,9 @@ async def Query(self, repo: Repository) -> AsyncGenerator[ResultInfo | ErrorInfo
async for info in self._GenerateSecurityAlertInfo(repo, github_url):
yield info

async for info in self._GenerateReleaseInfo(repo, github_url):
yield info

default_branch = persisted_info.get("default_branch")

if default_branch:
Expand Down Expand Up @@ -427,6 +431,107 @@ async def _GenerateCICDInfo(
except Exception as ex:
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GenerateReleaseInfo(
self,
repo: Repository,
github_url: str,
) -> AsyncGenerator[ResultInfo | ErrorInfo]:
key = (self.__class__.__name__, "release")

try:
url = f"https://api.github.com/repos/{repo.github_owner}/{repo.github_repo}/releases/latest"

async with self._session.get(url) as response:
if response.status == HTTPStatus.NOT_FOUND:
# No releases found
yield ResultInfo(
repo,
key,
"-",
textwrap.dedent(
"""\
Releases: {github_url}/releases

No releases found.
""",
).format(github_url=github_url),
)
return
Comment thread
davidbrownell marked this conversation as resolved.

response.raise_for_status()
release = await response.json()

tag_name = release.get("tag_name", "unknown")
release_name = release.get("name", tag_name)
published_at = release.get("published_at", "")
is_prerelease = release.get("prerelease", False)
is_draft = release.get("draft", False)
html_url = release.get("html_url", f"{github_url}/releases/latest")
author = release.get("author", {}).get("login", "unknown")

# Format the published date
if published_at:
try:
pub_date = datetime.fromisoformat(published_at)
date_str = pub_date.strftime("%Y-%m-%d")
except ValueError:
max_date_length = 10

date_str = (
published_at[:max_date_length]
if len(published_at) >= max_date_length
else published_at
)
else:
date_str = "unknown"

# Build display value
if is_draft:
display_value = f"{tag_name} πŸ“"
elif is_prerelease:
display_value = f"{tag_name} 🚧"
else:
display_value = f"{tag_name} 🏷️"

# Build additional info
additional_info_lines = [
f"Releases: {github_url}/releases",
"",
"Latest Release:",
f" Tag: {tag_name}",
f" Name: {release_name}",
f" Published: {date_str}",
f" Author: {author}",
f" URL: {html_url}",
]

if is_draft:
additional_info_lines.append(" Status: Draft")
elif is_prerelease:
additional_info_lines.append(" Status: Pre-release")
else:
additional_info_lines.append(" Status: Stable")

# Add asset information if available
if assets := release.get("assets", []):
additional_info_lines.extend(["", "Assets:"])

for asset in assets:
asset_name = asset.get("name", "unknown")
download_count = asset.get("download_count", 0)
additional_info_lines.append(f" - {asset_name} ({download_count} downloads)")

yield ResultInfo(
repo,
key,
display_value,
"\n".join(additional_info_lines),
)

except Exception as ex:
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GeneratePaginatedResults(self, url: str) -> AsyncGenerator[dict]:
params = {"state": "open", "per_page": 100}
Expand Down
4 changes: 3 additions & 1 deletion tests/MainApp_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
MainApp,
NameColumn,
PullRequestsColumn,
ReleaseColumn,
RemoteColumn,
SecurityAlertsColumn,
StarsColumn,
Expand Down Expand Up @@ -955,7 +956,7 @@ def test_cicd_status_column_properties(self) -> None:
def test_archived_column_properties(self) -> None:
"""ArchivedColumn has correct properties."""

assert ArchivedColumn.value == 12
assert ArchivedColumn.value == 13
assert ArchivedColumn.name == "Archived"
assert ArchivedColumn.justify == "center"

Expand All @@ -978,6 +979,7 @@ def test_column_map_contains_all_columns(self) -> None:
PullRequestsColumn,
SecurityAlertsColumn,
CICDStatusColumn,
ReleaseColumn,
ArchivedColumn,
}

Expand Down
12 changes: 7 additions & 5 deletions tests/Sources/GitHubSource_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async def test_query_returns_valid_results(

# Verify all display values contain numeric counts (for ResultInfo items)
for result in results:
if isinstance(result, ResultInfo) and result.key[1] not in ["cicd_status", "archived"]:
if isinstance(result, ResultInfo) and result.key[1] not in ["cicd_status", "archived", "release"]:
number_part = result.display_value.split()[0]
assert number_part.isdigit(), f"Expected number in '{result.display_value}'"

Expand All @@ -221,15 +221,17 @@ async def test_query_nonexistent_repo_returns_error(self, session: aiohttp.Clien

results = [info async for info in source.Query(repo)]

# Four API calls fail for nonexistent repos (stars, issues, pull_requests, security_alerts)
# Five API calls for nonexistent repos (stars, issues, pull_requests, security_alerts, release)
# CI/CD is skipped because we don't have default_branch when repo API fails
assert len(results) == 4
# Note: release returns ResultInfo with "-" when no releases found (404)
assert len(results) == 5
assert results[0].key == ("GitHubSource", "stars")
assert results[1].key == ("GitHubSource", "issues")
assert results[2].key == ("GitHubSource", "pull_requests")
assert results[3].key == ("GitHubSource", "security_alerts")
# All results are errors
assert all(isinstance(r, ErrorInfo) for r in results)
assert results[4].key == ("GitHubSource", "release")
# First four results are errors, release may be a ResultInfo (404 handled gracefully)
assert all(isinstance(r, ErrorInfo) for r in results[:4])


# ----------------------------------------------------------------------
Expand Down
Loading
Loading