webctx 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +97 -0
- package/CONTRIBUTORS.md +126 -0
- package/LICENSE +202 -0
- package/Makefile +69 -0
- package/README.md +121 -0
- package/bin/webctx.js +28 -0
- package/cmd/webctx/main.go +11 -0
- package/docs/porting-status.md +173 -0
- package/go.mod +3 -0
- package/internal/app/app.go +139 -0
- package/internal/app/app_test.go +194 -0
- package/internal/app/github.go +2084 -0
- package/internal/app/github_acceptance_test.go +166 -0
- package/internal/app/github_actions.go +1008 -0
- package/internal/app/github_actions_test.go +484 -0
- package/internal/app/github_activity_deployments.go +503 -0
- package/internal/app/github_activity_deployments_test.go +321 -0
- package/internal/app/github_commits.go +1004 -0
- package/internal/app/github_commits_test.go +503 -0
- package/internal/app/github_discussions_gists.go +1156 -0
- package/internal/app/github_discussions_gists_test.go +425 -0
- package/internal/app/github_issues.go +1616 -0
- package/internal/app/github_issues_test.go +731 -0
- package/internal/app/github_packages_projects.go +429 -0
- package/internal/app/github_packages_projects_test.go +168 -0
- package/internal/app/github_phase9_overview_stress_test.go +178 -0
- package/internal/app/github_pull_views.go +1141 -0
- package/internal/app/github_pull_views_test.go +603 -0
- package/internal/app/github_pulls.go +1459 -0
- package/internal/app/github_pulls_test.go +715 -0
- package/internal/app/github_refs.go +621 -0
- package/internal/app/github_refs_test.go +260 -0
- package/internal/app/github_search_profiles.go +505 -0
- package/internal/app/github_search_profiles_test.go +352 -0
- package/internal/app/github_test.go +888 -0
- package/internal/app/scrape.go +264 -0
- package/internal/app/tools.go +670 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/package.json +66 -0
- package/scripts/postinstall.js +137 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/url"
|
|
9
|
+
"sort"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type githubRepositoryActivity struct {
|
|
16
|
+
ID int64 `json:"id"`
|
|
17
|
+
NodeID string `json:"node_id"`
|
|
18
|
+
Before string `json:"before"`
|
|
19
|
+
After string `json:"after"`
|
|
20
|
+
Ref string `json:"ref"`
|
|
21
|
+
Timestamp string `json:"timestamp"`
|
|
22
|
+
ActivityType string `json:"activity_type"`
|
|
23
|
+
Actor githubUser `json:"actor"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type githubContributorStats struct {
|
|
27
|
+
Total int `json:"total"`
|
|
28
|
+
Author githubUser `json:"author"`
|
|
29
|
+
Weeks []struct {
|
|
30
|
+
Week int64 `json:"w"`
|
|
31
|
+
Additions int `json:"a"`
|
|
32
|
+
Deletions int `json:"d"`
|
|
33
|
+
Commits int `json:"c"`
|
|
34
|
+
} `json:"weeks"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type githubCommitActivityWeek struct {
|
|
38
|
+
Days []int `json:"days"`
|
|
39
|
+
Total int `json:"total"`
|
|
40
|
+
Week int64 `json:"week"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type githubDeployment struct {
|
|
44
|
+
ID int64 `json:"id"`
|
|
45
|
+
SHA string `json:"sha"`
|
|
46
|
+
Ref string `json:"ref"`
|
|
47
|
+
Task string `json:"task"`
|
|
48
|
+
Environment string `json:"environment"`
|
|
49
|
+
Description *string `json:"description"`
|
|
50
|
+
CreatedAt string `json:"created_at"`
|
|
51
|
+
UpdatedAt string `json:"updated_at"`
|
|
52
|
+
Creator githubUser `json:"creator"`
|
|
53
|
+
TransientEnvironment bool `json:"transient_environment"`
|
|
54
|
+
ProductionEnvironment bool `json:"production_environment"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type githubDeploymentStatus struct {
|
|
58
|
+
ID int64 `json:"id"`
|
|
59
|
+
State string `json:"state"`
|
|
60
|
+
Description string `json:"description"`
|
|
61
|
+
Environment string `json:"environment"`
|
|
62
|
+
LogURL string `json:"log_url"`
|
|
63
|
+
EnvironmentURL string `json:"environment_url"`
|
|
64
|
+
CreatedAt string `json:"created_at"`
|
|
65
|
+
UpdatedAt string `json:"updated_at"`
|
|
66
|
+
Creator githubUser `json:"creator"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type githubEnvironment struct {
|
|
70
|
+
ID int64 `json:"id"`
|
|
71
|
+
Name string `json:"name"`
|
|
72
|
+
HTMLURL string `json:"html_url"`
|
|
73
|
+
CreatedAt string `json:"created_at"`
|
|
74
|
+
UpdatedAt string `json:"updated_at"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type githubDeploymentLatestStatus struct {
|
|
78
|
+
Status *githubDeploymentStatus
|
|
79
|
+
ProviderMore bool
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func readGitHubActivity(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
83
|
+
if target.Fragment != "" {
|
|
84
|
+
return "", fmt.Errorf("GitHub activity fragment %q is not a supported native selector", target.Fragment)
|
|
85
|
+
}
|
|
86
|
+
allowed := map[string]struct{}{"ref": {}, "activity_type": {}, "actor": {}, "time_period": {}, "before": {}, "after": {}, "page": {}}
|
|
87
|
+
for key := range target.Query {
|
|
88
|
+
if _, ok := allowed[key]; !ok {
|
|
89
|
+
return "", fmt.Errorf("GitHub activity query parameter %q is not supported by the native reader", key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
q := copySelectedQuery(target.Query, []string{"ref", "activity_type", "actor", "time_period", "before", "after", "page"})
|
|
93
|
+
q.Set("per_page", strconv.Itoa(githubPageableListSize))
|
|
94
|
+
if page := q.Get("page"); page != "" {
|
|
95
|
+
n, err := strconv.Atoi(page)
|
|
96
|
+
if err != nil || n <= 0 {
|
|
97
|
+
return "", fmt.Errorf("invalid GitHub activity page %q", page)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/activity?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), q.Encode())
|
|
101
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
102
|
+
if err != nil {
|
|
103
|
+
return "", err
|
|
104
|
+
}
|
|
105
|
+
var items []githubRepositoryActivity
|
|
106
|
+
if err := json.Unmarshal(resp.Body, &items); err != nil {
|
|
107
|
+
return "", fmt.Errorf("decode GitHub repository activity: %w", err)
|
|
108
|
+
}
|
|
109
|
+
limit := len(items)
|
|
110
|
+
lines := listFrontmatter(target, "activity", len(items), limit)
|
|
111
|
+
lines = append(lines, "# Repository activity", "")
|
|
112
|
+
if len(items) == 0 {
|
|
113
|
+
lines = append(lines, "_No activity returned by GitHub on this page._")
|
|
114
|
+
}
|
|
115
|
+
for _, item := range items[:limit] {
|
|
116
|
+
activityType, _ := githubOverviewInlinePreview(item.ActivityType, 60)
|
|
117
|
+
line := "- " + activityType
|
|
118
|
+
if item.Actor.Login != "" {
|
|
119
|
+
actor, truncated := githubOverviewInlinePreview(item.Actor.Login, 80)
|
|
120
|
+
if truncated {
|
|
121
|
+
actor += "…"
|
|
122
|
+
}
|
|
123
|
+
line += " by @" + actor
|
|
124
|
+
}
|
|
125
|
+
if item.Ref != "" {
|
|
126
|
+
ref, truncated := githubOverviewInlinePreview(item.Ref, 120)
|
|
127
|
+
if truncated {
|
|
128
|
+
ref += "…"
|
|
129
|
+
}
|
|
130
|
+
line += " — `" + ref + "`"
|
|
131
|
+
}
|
|
132
|
+
if item.Before != "" || item.After != "" {
|
|
133
|
+
line += " — `" + shortSHA(item.Before) + "` → `" + shortSHA(item.After) + "`"
|
|
134
|
+
}
|
|
135
|
+
if item.Timestamp != "" {
|
|
136
|
+
line += " — " + item.Timestamp
|
|
137
|
+
}
|
|
138
|
+
lines = append(lines, line)
|
|
139
|
+
}
|
|
140
|
+
return appendListNavigation(lines, target, resp.Links()), nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func readGitHubContributorStats(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
144
|
+
resp, err := readGitHubStatistics(ctx, client, target, "contributors")
|
|
145
|
+
if err != nil || resp.StatusCode == http.StatusAccepted {
|
|
146
|
+
return renderStatisticsPending(target, "contributors", resp), err
|
|
147
|
+
}
|
|
148
|
+
var stats []githubContributorStats
|
|
149
|
+
if err := json.Unmarshal(resp.Body, &stats); err != nil {
|
|
150
|
+
return "", fmt.Errorf("decode GitHub contributor statistics: %w", err)
|
|
151
|
+
}
|
|
152
|
+
sort.SliceStable(stats, func(i, j int) bool {
|
|
153
|
+
if stats[i].Total != stats[j].Total {
|
|
154
|
+
return stats[i].Total > stats[j].Total
|
|
155
|
+
}
|
|
156
|
+
left := strings.ToLower(stats[i].Author.Login)
|
|
157
|
+
right := strings.ToLower(stats[j].Author.Login)
|
|
158
|
+
if left == right {
|
|
159
|
+
return stats[i].Author.Login < stats[j].Author.Login
|
|
160
|
+
}
|
|
161
|
+
return left < right
|
|
162
|
+
})
|
|
163
|
+
return renderGitHubContributorStats(target, stats), nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func renderGitHubContributorStats(target *GitHubTarget, stats []githubContributorStats) string {
|
|
167
|
+
limit := minInt(70, len(stats))
|
|
168
|
+
for {
|
|
169
|
+
out := renderGitHubContributorStatsWithLimit(target, stats, limit)
|
|
170
|
+
if githubOverviewFits(out) || limit <= 1 {
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
limit--
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func renderGitHubContributorStatsWithLimit(target *GitHubTarget, stats []githubContributorStats, limit int) string {
|
|
178
|
+
limit = minInt(limit, len(stats))
|
|
179
|
+
lines := []string{
|
|
180
|
+
"---",
|
|
181
|
+
"repository: " + yamlScalar(target.Owner+"/"+target.Repo),
|
|
182
|
+
"view: contributors",
|
|
183
|
+
fmt.Sprintf("contributors_returned: %d", len(stats)),
|
|
184
|
+
fmt.Sprintf("contributors_indexed: %d", limit),
|
|
185
|
+
fmt.Sprintf("contributors_local_omitted: %d", len(stats)-limit),
|
|
186
|
+
"---",
|
|
187
|
+
"",
|
|
188
|
+
"# Contributor statistics",
|
|
189
|
+
"",
|
|
190
|
+
statisticsFreshnessNote(),
|
|
191
|
+
"",
|
|
192
|
+
}
|
|
193
|
+
for _, contributor := range stats[:limit] {
|
|
194
|
+
adds, dels := 0, 0
|
|
195
|
+
for _, week := range contributor.Weeks {
|
|
196
|
+
adds += week.Additions
|
|
197
|
+
dels += week.Deletions
|
|
198
|
+
}
|
|
199
|
+
name := contributor.Author.Login
|
|
200
|
+
if name == "" {
|
|
201
|
+
name = "unknown"
|
|
202
|
+
}
|
|
203
|
+
lines = append(lines, fmt.Sprintf("- @%s — %d commits · +%d -%d", name, contributor.Total, adds, dels))
|
|
204
|
+
}
|
|
205
|
+
if note := githubLocalOmissionNote("contributors returned by GitHub", len(stats)-limit); note != "" {
|
|
206
|
+
lines = append(lines, "", note)
|
|
207
|
+
}
|
|
208
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func readGitHubCommitActivityStats(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
212
|
+
resp, err := readGitHubStatistics(ctx, client, target, "commit_activity")
|
|
213
|
+
if err != nil || resp.StatusCode == http.StatusAccepted {
|
|
214
|
+
return renderStatisticsPending(target, "commit activity", resp), err
|
|
215
|
+
}
|
|
216
|
+
var weeks []githubCommitActivityWeek
|
|
217
|
+
if err := json.Unmarshal(resp.Body, &weeks); err != nil {
|
|
218
|
+
return "", fmt.Errorf("decode GitHub commit activity statistics: %w", err)
|
|
219
|
+
}
|
|
220
|
+
lines := statisticsFrontmatter(target, "commit_activity", len(weeks))
|
|
221
|
+
lines = append(lines, "# Commit activity", "", statisticsFreshnessNote(), "")
|
|
222
|
+
for _, week := range weeks {
|
|
223
|
+
stamp := time.Unix(week.Week, 0).UTC().Format("2006-01-02")
|
|
224
|
+
lines = append(lines, fmt.Sprintf("- %s — %d commits", stamp, week.Total))
|
|
225
|
+
}
|
|
226
|
+
return strings.TrimSpace(strings.Join(lines, "\n")), nil
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func readGitHubCodeFrequencyStats(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
230
|
+
resp, err := readGitHubStatistics(ctx, client, target, "code_frequency")
|
|
231
|
+
if err != nil || resp.StatusCode == http.StatusAccepted {
|
|
232
|
+
return renderStatisticsPending(target, "code frequency", resp), err
|
|
233
|
+
}
|
|
234
|
+
var weeks [][]int64
|
|
235
|
+
if err := json.Unmarshal(resp.Body, &weeks); err != nil {
|
|
236
|
+
return "", fmt.Errorf("decode GitHub code-frequency statistics: %w", err)
|
|
237
|
+
}
|
|
238
|
+
return renderGitHubCodeFrequencyStats(target, weeks), nil
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func renderGitHubCodeFrequencyStats(target *GitHubTarget, rawWeeks [][]int64) string {
|
|
242
|
+
weeks := make([][]int64, 0, len(rawWeeks))
|
|
243
|
+
var additionsTotal, deletionsTotal int64
|
|
244
|
+
for _, week := range rawWeeks {
|
|
245
|
+
if len(week) < 3 {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
weeks = append(weeks, week)
|
|
249
|
+
additionsTotal += week[1]
|
|
250
|
+
deletionsTotal += week[2]
|
|
251
|
+
}
|
|
252
|
+
sort.SliceStable(weeks, func(i, j int) bool { return weeks[i][0] < weeks[j][0] })
|
|
253
|
+
limit := minInt(52, len(weeks))
|
|
254
|
+
start := len(weeks) - limit
|
|
255
|
+
lines := []string{
|
|
256
|
+
"---",
|
|
257
|
+
"repository: " + yamlScalar(target.Owner+"/"+target.Repo),
|
|
258
|
+
"view: code_frequency",
|
|
259
|
+
fmt.Sprintf("weeks_returned: %d", len(weeks)),
|
|
260
|
+
fmt.Sprintf("weeks_indexed: %d", limit),
|
|
261
|
+
fmt.Sprintf("weeks_local_omitted: %d", start),
|
|
262
|
+
fmt.Sprintf("additions_total: %d", additionsTotal),
|
|
263
|
+
fmt.Sprintf("deletions_total: %d", deletionsTotal),
|
|
264
|
+
"---",
|
|
265
|
+
"",
|
|
266
|
+
"# Code frequency",
|
|
267
|
+
"",
|
|
268
|
+
statisticsFreshnessNote(),
|
|
269
|
+
"",
|
|
270
|
+
}
|
|
271
|
+
if len(weeks) == 0 {
|
|
272
|
+
lines = append(lines, "_No code-frequency weeks returned by GitHub._")
|
|
273
|
+
}
|
|
274
|
+
if start > 0 {
|
|
275
|
+
lines = append(lines, fmt.Sprintf("> Showing the most recent %d weekly buckets; %d older weekly buckets returned by GitHub are locally omitted from this overview. Aggregate totals above include every returned bucket.", limit, start), "")
|
|
276
|
+
}
|
|
277
|
+
for _, week := range weeks[start:] {
|
|
278
|
+
stamp := time.Unix(week[0], 0).UTC().Format("2006-01-02")
|
|
279
|
+
lines = append(lines, fmt.Sprintf("- %s — +%d %d", stamp, week[1], week[2]))
|
|
280
|
+
}
|
|
281
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func readGitHubStatistics(ctx context.Context, client *GitHubClient, target *GitHubTarget, kind string) (GitHubResponse, error) {
|
|
285
|
+
if target.Fragment != "" || len(target.Query) > 0 {
|
|
286
|
+
return GitHubResponse{}, fmt.Errorf("GitHub statistics views do not support query/fragment selectors in the native reader")
|
|
287
|
+
}
|
|
288
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/stats/%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), kind)
|
|
289
|
+
return client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func renderStatisticsPending(target *GitHubTarget, view string, resp GitHubResponse) string {
|
|
293
|
+
if resp.StatusCode != http.StatusAccepted {
|
|
294
|
+
return ""
|
|
295
|
+
}
|
|
296
|
+
return fmt.Sprintf("---\nrepository: %s\nview: %s\nprovider_status: computing\n---\n\n# %s\n\nGitHub returned HTTP 202 while computing cached repository statistics. Retry the same URL later; webctx has no local cache and cannot make the provider compute them synchronously.", yamlScalar(target.Owner+"/"+target.Repo), yamlScalar(view), strings.Title(view))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func statisticsFrontmatter(target *GitHubTarget, view string, returned int) []string {
|
|
300
|
+
return []string{"---", "repository: " + yamlScalar(target.Owner+"/"+target.Repo), "view: " + yamlScalar(view), fmt.Sprintf("returned: %d", returned), "---", ""}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func statisticsFreshnessNote() string {
|
|
304
|
+
return "_GitHub computes/caches repository statistics upstream. This command made a fresh provider request, but the statistics themselves may reflect GitHub's cached computation._"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func readGitHubDeployments(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
308
|
+
if target.Fragment != "" {
|
|
309
|
+
return "", fmt.Errorf("GitHub deployments fragment %q is not a supported native selector", target.Fragment)
|
|
310
|
+
}
|
|
311
|
+
allowed := map[string]struct{}{"sha": {}, "ref": {}, "task": {}, "environment": {}, "page": {}}
|
|
312
|
+
for key := range target.Query {
|
|
313
|
+
if _, ok := allowed[key]; !ok {
|
|
314
|
+
return "", fmt.Errorf("GitHub deployments query parameter %q is not supported by the native reader", key)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
q := copySelectedQuery(target.Query, []string{"sha", "ref", "task", "environment", "page"})
|
|
318
|
+
q.Set("per_page", strconv.Itoa(githubPageableListSize))
|
|
319
|
+
resp, deployments, err := fetchGitHubDeploymentPage(ctx, client, target, q)
|
|
320
|
+
if err != nil {
|
|
321
|
+
return "", err
|
|
322
|
+
}
|
|
323
|
+
limit := len(deployments)
|
|
324
|
+
lines := listFrontmatter(target, "deployments", len(deployments), limit)
|
|
325
|
+
lines = append(lines, "# Deployments", "")
|
|
326
|
+
lines = append(lines, renderDeploymentRows(target, deployments[:limit])...)
|
|
327
|
+
return appendListNavigation(lines, target, resp.Links()), nil
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func readGitHubDeploymentEnvironment(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
331
|
+
if target.Fragment != "" {
|
|
332
|
+
return "", fmt.Errorf("GitHub deployment environment fragment %q is not a supported native selector", target.Fragment)
|
|
333
|
+
}
|
|
334
|
+
for key := range target.Query {
|
|
335
|
+
if key != "page" {
|
|
336
|
+
return "", fmt.Errorf("GitHub deployment environment query parameter %q is not supported", key)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
envEndpoint := fmt.Sprintf("/repos/%s/%s/environments/%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), url.PathEscape(target.Name))
|
|
340
|
+
envResp, err := client.REST(ctx, http.MethodGet, envEndpoint, "application/vnd.github+json")
|
|
341
|
+
if err != nil {
|
|
342
|
+
return "", err
|
|
343
|
+
}
|
|
344
|
+
var environment githubEnvironment
|
|
345
|
+
if err := json.Unmarshal(envResp.Body, &environment); err != nil {
|
|
346
|
+
return "", fmt.Errorf("decode GitHub environment: %w", err)
|
|
347
|
+
}
|
|
348
|
+
q := url.Values{"environment": []string{target.Name}, "per_page": []string{strconv.Itoa(githubPageableListSize)}}
|
|
349
|
+
if page := target.Query.Get("page"); page != "" {
|
|
350
|
+
n, err := strconv.Atoi(page)
|
|
351
|
+
if err != nil || n <= 0 {
|
|
352
|
+
return "", fmt.Errorf("invalid GitHub deployment environment page %q", page)
|
|
353
|
+
}
|
|
354
|
+
q.Set("page", page)
|
|
355
|
+
}
|
|
356
|
+
depResp, deployments, err := fetchGitHubDeploymentPage(ctx, client, target, q)
|
|
357
|
+
if err != nil {
|
|
358
|
+
return "", err
|
|
359
|
+
}
|
|
360
|
+
latestStatuses := make([]githubDeploymentLatestStatus, 0, len(deployments))
|
|
361
|
+
for _, deployment := range deployments {
|
|
362
|
+
latest, providerMore, err := fetchGitHubLatestDeploymentStatus(ctx, client, target, deployment.ID)
|
|
363
|
+
if err != nil {
|
|
364
|
+
return "", err
|
|
365
|
+
}
|
|
366
|
+
latestStatuses = append(latestStatuses, githubDeploymentLatestStatus{Status: latest, ProviderMore: providerMore})
|
|
367
|
+
}
|
|
368
|
+
return renderGitHubDeploymentEnvironment(target, environment, deployments, latestStatuses, depResp.Links()), nil
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func renderGitHubDeploymentEnvironment(target *GitHubTarget, environment githubEnvironment, deployments []githubDeployment, latestStatuses []githubDeploymentLatestStatus, links GitHubLinkRelations) string {
|
|
372
|
+
return renderGitHubDeploymentEnvironmentWithLimit(target, environment, deployments, latestStatuses, links, len(deployments))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func renderGitHubDeploymentEnvironmentWithLimit(target *GitHubTarget, environment githubEnvironment, deployments []githubDeployment, latestStatuses []githubDeploymentLatestStatus, links GitHubLinkRelations, limit int) string {
|
|
376
|
+
limit = minInt(limit, len(deployments))
|
|
377
|
+
olderCount := 0
|
|
378
|
+
for _, latest := range latestStatuses {
|
|
379
|
+
if latest.ProviderMore {
|
|
380
|
+
olderCount++
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
lines := []string{"---", "repository: " + yamlScalar(target.Owner+"/"+target.Repo), "environment: " + yamlScalar(target.Name), fmt.Sprintf("deployments_returned: %d", len(deployments)), fmt.Sprintf("deployments_indexed: %d", limit), fmt.Sprintf("deployments_local_omitted: %d", len(deployments)-limit), fmt.Sprintf("deployments_with_older_statuses: %d", olderCount)}
|
|
384
|
+
if environment.CreatedAt != "" {
|
|
385
|
+
lines = append(lines, "created: "+yamlScalar(environment.CreatedAt))
|
|
386
|
+
}
|
|
387
|
+
if environment.UpdatedAt != "" {
|
|
388
|
+
lines = append(lines, "updated: "+yamlScalar(environment.UpdatedAt))
|
|
389
|
+
}
|
|
390
|
+
lines = append(lines, "---", "", "# Deployment environment: "+target.Name, "", "_GitHub controls deployment-status retention. webctx reads only the latest returned status for each deployment in this bounded overview; older status history may remain available upstream._", "")
|
|
391
|
+
for i, deployment := range deployments[:limit] {
|
|
392
|
+
lines = append(lines, renderDeploymentHeading(deployment), "")
|
|
393
|
+
if i >= len(latestStatuses) || latestStatuses[i].Status == nil {
|
|
394
|
+
lines = append(lines, "_No deployment statuses returned by GitHub._", "")
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
latest := latestStatuses[i]
|
|
398
|
+
status := *latest.Status
|
|
399
|
+
line := fmt.Sprintf("- latest status %d — %s", status.ID, status.State)
|
|
400
|
+
if status.Description != "" {
|
|
401
|
+
preview, truncated := githubOverviewPreview(status.Description, 160)
|
|
402
|
+
line += " — " + preview
|
|
403
|
+
if truncated {
|
|
404
|
+
line += "…"
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if status.Creator.Login != "" {
|
|
408
|
+
line += " — @" + status.Creator.Login
|
|
409
|
+
}
|
|
410
|
+
if status.CreatedAt != "" {
|
|
411
|
+
line += " — " + status.CreatedAt
|
|
412
|
+
}
|
|
413
|
+
if status.LogURL != "" {
|
|
414
|
+
line += " — logs " + status.LogURL
|
|
415
|
+
}
|
|
416
|
+
lines = append(lines, line)
|
|
417
|
+
if status.EnvironmentURL != "" {
|
|
418
|
+
lines = append(lines, " Environment: "+status.EnvironmentURL)
|
|
419
|
+
}
|
|
420
|
+
if latest.ProviderMore {
|
|
421
|
+
lines = append(lines, " Older statuses: available upstream; not expanded in this overview.")
|
|
422
|
+
}
|
|
423
|
+
lines = append(lines, "")
|
|
424
|
+
}
|
|
425
|
+
if note := githubLocalOmissionNote("deployments returned on this environment page", len(deployments)-limit); note != "" {
|
|
426
|
+
lines = append(lines, note, "")
|
|
427
|
+
}
|
|
428
|
+
if nav := renderGitHubUIPageNavigation(target, links); len(nav) > 0 {
|
|
429
|
+
lines = append(lines, "## Navigation", "")
|
|
430
|
+
lines = append(lines, nav...)
|
|
431
|
+
}
|
|
432
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func fetchGitHubDeploymentPage(ctx context.Context, client *GitHubClient, target *GitHubTarget, q url.Values) (GitHubResponse, []githubDeployment, error) {
|
|
436
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/deployments?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), q.Encode())
|
|
437
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
438
|
+
if err != nil {
|
|
439
|
+
return GitHubResponse{}, nil, err
|
|
440
|
+
}
|
|
441
|
+
var deployments []githubDeployment
|
|
442
|
+
if err := json.Unmarshal(resp.Body, &deployments); err != nil {
|
|
443
|
+
return GitHubResponse{}, nil, fmt.Errorf("decode GitHub deployments: %w", err)
|
|
444
|
+
}
|
|
445
|
+
return resp, deployments, nil
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
func fetchGitHubLatestDeploymentStatus(ctx context.Context, client *GitHubClient, target *GitHubTarget, deploymentID int64) (*githubDeploymentStatus, bool, error) {
|
|
449
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/deployments/%d/statuses?per_page=1", url.PathEscape(target.Owner), url.PathEscape(target.Repo), deploymentID)
|
|
450
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
451
|
+
if err != nil {
|
|
452
|
+
return nil, false, err
|
|
453
|
+
}
|
|
454
|
+
var statuses []githubDeploymentStatus
|
|
455
|
+
if err := json.Unmarshal(resp.Body, &statuses); err != nil {
|
|
456
|
+
return nil, false, fmt.Errorf("decode GitHub deployment statuses: %w", err)
|
|
457
|
+
}
|
|
458
|
+
if len(statuses) == 0 {
|
|
459
|
+
return nil, resp.Links()["next"] != "", nil
|
|
460
|
+
}
|
|
461
|
+
return &statuses[0], resp.Links()["next"] != "", nil
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
func renderDeploymentRows(target *GitHubTarget, deployments []githubDeployment) []string {
|
|
465
|
+
if len(deployments) == 0 {
|
|
466
|
+
return []string{"_No deployments returned by GitHub._"}
|
|
467
|
+
}
|
|
468
|
+
lines := []string{}
|
|
469
|
+
for _, deployment := range deployments {
|
|
470
|
+
line := "- " + renderDeploymentHeading(deployment)
|
|
471
|
+
if deployment.Environment != "" {
|
|
472
|
+
href := fmt.Sprintf("https://github.com/%s/%s/deployments/%s", escapePathPreservingSlashes(target.Owner), escapePathPreservingSlashes(target.Repo), escapePathPreservingSlashes(deployment.Environment))
|
|
473
|
+
environment, truncated := githubOverviewInlinePreview(deployment.Environment, 100)
|
|
474
|
+
if truncated {
|
|
475
|
+
environment += "…"
|
|
476
|
+
}
|
|
477
|
+
line += " — [" + escapeMarkdownLinkText(environment) + "](" + href + ")"
|
|
478
|
+
}
|
|
479
|
+
lines = append(lines, line)
|
|
480
|
+
}
|
|
481
|
+
return lines
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
func renderDeploymentHeading(deployment githubDeployment) string {
|
|
485
|
+
line := fmt.Sprintf("Deployment %d", deployment.ID)
|
|
486
|
+
if deployment.Ref != "" {
|
|
487
|
+
ref, truncated := githubOverviewInlinePreview(deployment.Ref, 120)
|
|
488
|
+
if truncated {
|
|
489
|
+
ref += "…"
|
|
490
|
+
}
|
|
491
|
+
line += " — `" + ref + "`"
|
|
492
|
+
}
|
|
493
|
+
if deployment.SHA != "" {
|
|
494
|
+
line += " @ `" + shortSHA(deployment.SHA) + "`"
|
|
495
|
+
}
|
|
496
|
+
if deployment.Creator.Login != "" {
|
|
497
|
+
line += " — @" + deployment.Creator.Login
|
|
498
|
+
}
|
|
499
|
+
if deployment.CreatedAt != "" {
|
|
500
|
+
line += " — " + deployment.CreatedAt
|
|
501
|
+
}
|
|
502
|
+
return line
|
|
503
|
+
}
|