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
0 commit comments