Skip to content

Commit fdebe45

Browse files
committed
Add ecosystem status page
1 parent 85d19d8 commit fdebe45

File tree

5 files changed

+459
-1
lines changed

5 files changed

+459
-1
lines changed

_includes/footer.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@
6969
</span>
7070
</a>
7171
</li>
72+
<li class="list__item">
73+
<a href="/ecosystem-status/" class="list__item__link link link--type-1">
74+
<span class="link__text">
75+
Ecosystem Status
76+
</span>
77+
</a>
78+
</li>
7279
</ul>
7380
</div>
7481
</div>

_plugins/ecosystem_status.rb

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
require 'json'
2+
require 'net/http'
3+
require 'uri'
4+
require 'time'
5+
6+
module Jekyll
7+
class EcosystemStatusGenerator < Generator
8+
safe true
9+
priority :high
10+
11+
def generate(site)
12+
github_token = ENV['GITHUB_TOKEN']
13+
headers = {}
14+
if github_token && !github_token.empty?
15+
headers = {
16+
'Authorization' => "token #{github_token}",
17+
'Accept' => 'application/vnd.github.v3+json',
18+
'User-Agent' => 'tskit-ecosystem-status'
19+
}
20+
else
21+
puts "Warning: No GITHUB_TOKEN provided, ecosystem status data will be limited"
22+
headers = {
23+
'Accept' => 'application/vnd.github.v3+json',
24+
'User-Agent' => 'tskit-ecosystem-status'
25+
}
26+
end
27+
28+
site.data['ecosystem_status'] = {}
29+
30+
site.collections['software'].docs.each do |software|
31+
repo_name = software.data['name']
32+
gh_org = software.data['gh_org']
33+
python_package = software.data['python_package']
34+
35+
next unless gh_org && repo_name
36+
37+
repo_data = {}
38+
39+
begin
40+
# Get repository info
41+
repo_info = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}", headers)
42+
43+
# Get releases
44+
releases = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/releases", headers)
45+
latest_release = releases.first if releases && !releases.empty?
46+
47+
# Get commit info for main branch
48+
commits = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/commits?sha=main&per_page=1", headers)
49+
latest_commit = commits.first if commits && !commits.empty?
50+
51+
# Get PR info
52+
open_prs = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/pulls?state=open", headers)
53+
54+
# Get last merged PR - use the merged state and sort by merged date
55+
merged_prs = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/pulls?state=closed&sort=updated&direction=desc&per_page=50", headers)
56+
last_merged_pr = nil
57+
if merged_prs && !merged_prs.empty?
58+
# Find the most recently merged PR (not just closed)
59+
merged_only = merged_prs.select { |pr| pr['merged_at'] }
60+
last_merged_pr = merged_only.max_by { |pr| Time.parse(pr['merged_at']) } unless merged_only.empty?
61+
end
62+
63+
# Get CI status for latest commit (check both status API and check runs)
64+
ci_status = nil
65+
if latest_commit
66+
# Try GitHub Actions Check Runs first (modern approach)
67+
check_runs = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/commits/#{latest_commit['sha']}/check-runs", headers)
68+
if check_runs && check_runs['check_runs'] && !check_runs['check_runs'].empty?
69+
# Use check runs (GitHub Actions)
70+
check_states = check_runs['check_runs'].map { |run| run['conclusion'] || run['status'] }
71+
if check_states.any? { |state| state == 'failure' || state == 'cancelled' || state == 'timed_out' }
72+
ci_status = { 'state' => 'failure' }
73+
elsif check_states.any? { |state| state == 'in_progress' || state == 'queued' || state == 'pending' }
74+
ci_status = { 'state' => 'pending' }
75+
elsif check_states.all? { |state| state == 'success' || state == 'completed' }
76+
ci_status = { 'state' => 'success' }
77+
else
78+
ci_status = { 'state' => 'unknown' }
79+
end
80+
else
81+
# Fallback to older status API
82+
combined_status = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/commits/#{latest_commit['sha']}/status", headers)
83+
if combined_status
84+
ci_status = {
85+
'state' => combined_status['state'],
86+
'total_count' => combined_status['total_count'],
87+
'statuses' => combined_status['statuses']
88+
}
89+
end
90+
end
91+
end
92+
93+
# Calculate commits since last release
94+
commits_since_release = 0
95+
if latest_release && latest_commit
96+
release_commit_sha = latest_release['target_commitish'] || 'main'
97+
comparison = fetch_github_api("https://api.github.com/repos/#{gh_org}/#{repo_name}/compare/#{latest_release['tag_name']}...main", headers)
98+
commits_since_release = comparison['ahead_by'] if comparison
99+
end
100+
101+
# Get PyPI info if python package exists
102+
pypi_info = nil
103+
if python_package
104+
pypi_info = fetch_pypi_info(python_package)
105+
end
106+
107+
repo_data = {
108+
'repo_name' => repo_name,
109+
'gh_org' => gh_org,
110+
'python_package' => python_package,
111+
'repo_url' => "https://github.com/#{gh_org}/#{repo_name}",
112+
'latest_release' => latest_release,
113+
'commits_since_release' => commits_since_release,
114+
'latest_commit' => latest_commit,
115+
'ci_status' => ci_status,
116+
'open_pr_count' => open_prs ? open_prs.length : 0,
117+
'last_merged_pr' => last_merged_pr,
118+
'pypi_info' => pypi_info,
119+
'updated_at' => Time.now
120+
}
121+
122+
rescue => e
123+
puts "Error fetching data for #{gh_org}/#{repo_name}: #{e.message}"
124+
repo_data = {
125+
'repo_name' => repo_name,
126+
'gh_org' => gh_org,
127+
'error' => e.message,
128+
'updated_at' => Time.now
129+
}
130+
end
131+
132+
site.data['ecosystem_status'][repo_name] = repo_data
133+
end
134+
end
135+
136+
private
137+
138+
def fetch_github_api(url, headers)
139+
uri = URI(url)
140+
http = Net::HTTP.new(uri.host, uri.port)
141+
http.use_ssl = true
142+
143+
request = Net::HTTP::Get.new(uri)
144+
headers.each { |key, value| request[key] = value }
145+
146+
response = http.request(request)
147+
148+
if response.code == '200'
149+
JSON.parse(response.body)
150+
else
151+
puts "GitHub API error for #{url}: #{response.code} #{response.message}"
152+
nil
153+
end
154+
end
155+
156+
def fetch_pypi_info(package_name)
157+
uri = URI("https://pypi.org/pypi/#{package_name}/json")
158+
http = Net::HTTP.new(uri.host, uri.port)
159+
http.use_ssl = true
160+
161+
request = Net::HTTP::Get.new(uri)
162+
response = http.request(request)
163+
164+
if response.code == '200'
165+
JSON.parse(response.body)
166+
else
167+
nil
168+
end
169+
rescue => e
170+
puts "Error fetching PyPI info for #{package_name}: #{e.message}"
171+
nil
172+
end
173+
end
174+
end
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Ecosystem Status Table Component
2+
3+
.ecosystem-status {
4+
5+
&__table {
6+
background-color: #f8f9fa;
7+
8+
th, td {
9+
padding: 12px !important;
10+
}
11+
12+
th {
13+
&.center,
14+
&.ecosystem-status__header--center {
15+
text-align: center;
16+
}
17+
}
18+
19+
td {
20+
&.center,
21+
&.ecosystem-status__cell--center {
22+
text-align: center;
23+
}
24+
}
25+
}
26+
27+
&__unreleased-commits {
28+
&--high {
29+
color: var(--bs-danger) !important;
30+
font-weight: bold;
31+
}
32+
33+
&--moderate {
34+
color: var(--bs-warning) !important;
35+
font-weight: bold;
36+
}
37+
38+
&--low {
39+
color: var(--bs-info) !important;
40+
font-weight: bold;
41+
}
42+
43+
&--none {
44+
color: var(--bs-success) !important;
45+
font-weight: bold;
46+
}
47+
}
48+
49+
&__pr-count {
50+
&--high {
51+
color: var(--bs-warning) !important;
52+
font-weight: bold;
53+
}
54+
55+
&--moderate {
56+
color: var(--bs-info) !important;
57+
font-weight: bold;
58+
}
59+
60+
&--none {
61+
color: var(--bs-success) !important;
62+
font-weight: bold;
63+
}
64+
}
65+
66+
&__ci-status {
67+
&--success {
68+
color: var(--bs-success) !important;
69+
}
70+
71+
&--failure {
72+
color: var(--bs-danger) !important;
73+
}
74+
75+
&--pending {
76+
color: var(--bs-warning) !important;
77+
}
78+
79+
&--unknown {
80+
color: var(--bs-secondary) !important;
81+
}
82+
}
83+
84+
&__time-indicator {
85+
&--recent {
86+
color: var(--bs-success) !important;
87+
font-weight: bold;
88+
}
89+
90+
&--moderate {
91+
color: var(--bs-info) !important;
92+
font-weight: bold;
93+
}
94+
95+
&--old {
96+
color: var(--bs-warning) !important;
97+
font-weight: bold;
98+
}
99+
}
100+
}

assets/src/sass/style.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
//@import "~bootstrap/scss/popover";
4141
//@import "~bootstrap/scss/progress";
4242
//@import "~bootstrap/scss/spinners";
43-
//@import "~bootstrap/scss/tables";
43+
@import "~bootstrap/scss/tables";
4444
//@import "~bootstrap/scss/toasts";
4545
//@import "~bootstrap/scss/tooltip";
4646
@import "~bootstrap/scss/transitions";
@@ -61,6 +61,7 @@
6161
@import "components/call-to-action";
6262
@import "components/card";
6363
@import "components/code";
64+
@import "components/ecosystem-status";
6465
@import "components/footer";
6566
@import "components/forms";
6667
@import "components/header";

0 commit comments

Comments
 (0)