-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Description
Describe the feature or problem you’d like to solve
I would like to analyze performance characteristics of GitHub Actions workflows within repositories in order to identify expensive areas within automation. One of said areas is workflows with steps that take a long time, which are arguably expensive. However, gh run view --json jobs command only lists name, conclusion, step number, and status of each step.
Example: gh run view 11365476998 --json jobs --repo cli/cli
results in:
{
"jobs": [
{
"completedAt": "2024-10-16T12:23:29Z",
"conclusion": "success",
"databaseId": 31613745137,
"name": "linux",
"startedAt": "2024-10-16T12:19:46Z",
"status": "completed",
"steps": [
{
"conclusion": "success",
"name": "Set up job",
"number": 1,
"status": "completed"
},
{
"conclusion": "success",
"name": "Checkout",
"number": 2,
"status": "completed"
},
{
"conclusion": "success",
"name": "Set up Go",
"number": 3,
"status": "completed"
},
...Proposed solution
My suggestion is to enhance the Step struct used for retrieving data from GitHub API to include completedAt and startedAt fields defined within List jobs for a workflow run attempt endpoint.
This information is already being provided by GitHub API calls which can be seen via GH_DEBUG=api gh run view 11365476998 --json jobs --repo cli/cli:
* Request to https://api.github.com/repos/cli/cli/actions/runs/11365476998/jobs?per_page=100
> GET /repos/cli/cli/actions/runs/11365476998/jobs?per_page=100 HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/New_York
> User-Agent: GitHub CLI 2.58.0
⣻< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Cache-Control: private, max-age=60, s-maxage=60
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 16 Oct 2024 12:39:06 GMT
< Etag: W/"ce75646c66c3ae20c538110930f5d0562067629a033f7e17c53c134ffebc1f4f"
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes:
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Api-Version-Selected: 2022-11-28
< X-Github-Media-Type: github.v3; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: F3F2:3641A8:11BF99A:22CE595:670FB3EA
< X-Oauth-Client-Id: 178c6fc778ccc68e1d6a
< X-Oauth-Scopes: gist, read:org, repo, workflow
< X-Ratelimit-Limit: 15000
< X-Ratelimit-Remaining: 14973
< X-Ratelimit-Reset: 1729083896
< X-Ratelimit-Resource: core
< X-Ratelimit-Used: 27
< X-Xss-Protection: 0
{
"total_count": 4,
"jobs": [
{
"id": 31613745137,
"run_id": 11365476998,
"workflow_name": "v2.59.0 / production",
"head_branch": "trunk",
"run_url": "https://api.github.com/repos/cli/cli/actions/runs/11365476998",
"run_attempt": 1,
"node_id": "CR_kwDODKw3uc8AAAAHXFN38Q",
"head_sha": "7aef6ec39137adb601d31d13fce8b6f26b4903fa",
"url": "https://api.github.com/repos/cli/cli/actions/jobs/31613745137",
"html_url": "https://github.com/cli/cli/actions/runs/11365476998/job/31613745137",
"status": "completed",
"conclusion": "success",
"created_at": "2024-10-16T12:19:01Z",
"started_at": "2024-10-16T12:19:46Z",
"completed_at": "2024-10-16T12:23:29Z",
"name": "linux",
"steps": [
{
"name": "Set up job",
"status": "completed",
"conclusion": "success",
"number": 1,
"started_at": "2024-10-16T12:19:45Z",
"completed_at": "2024-10-16T12:19:47Z"
},
{
"name": "Checkout",
"status": "completed",
"conclusion": "success",
"number": 2,
"started_at": "2024-10-16T12:19:47Z",
"completed_at": "2024-10-16T12:19:48Z"
},
{
"name": "Set up Go",
"status": "completed",
"conclusion": "success",
"number": 3,
"started_at": "2024-10-16T12:19:48Z",
"completed_at": "2024-10-16T12:20:05Z"
},
{
"name": "Install GoReleaser",
"status": "completed",
"conclusion": "success",
"number": 4,
"started_at": "2024-10-16T12:20:05Z",
"completed_at": "2024-10-16T12:20:06Z"
},
{
"name": "Build release binaries",
"status": "completed",
"conclusion": "success",
"number": 5,
"started_at": "2024-10-16T12:20:06Z",
"completed_at": "2024-10-16T12:23:13Z"
},Additional context
I imagine exporting step datetimes has the same conditional requirement as job datetimes of dealing with missing / empty / zero completedAt information:
cli/pkg/cmd/run/shared/shared.go
Lines 178 to 233 in 7aef6ec
| case "jobs": | |
| jobs := make([]interface{}, 0, len(r.Jobs)) | |
| for _, j := range r.Jobs { | |
| steps := make([]interface{}, 0, len(j.Steps)) | |
| for _, s := range j.Steps { | |
| steps = append(steps, map[string]interface{}{ | |
| "name": s.Name, | |
| "status": s.Status, | |
| "conclusion": s.Conclusion, | |
| "number": s.Number, | |
| }) | |
| } | |
| var completedAt time.Time | |
| if !j.CompletedAt.IsZero() { | |
| completedAt = j.CompletedAt | |
| } | |
| jobs = append(jobs, map[string]interface{}{ | |
| "databaseId": j.ID, | |
| "status": j.Status, | |
| "conclusion": j.Conclusion, | |
| "name": j.Name, | |
| "steps": steps, | |
| "startedAt": j.StartedAt, | |
| "completedAt": completedAt, | |
| "url": j.URL, | |
| }) | |
| } | |
| data[f] = jobs | |
| default: | |
| sf := fieldByName(v, f) | |
| data[f] = sf.Interface() | |
| } | |
| } | |
| return data | |
| } | |
| type Job struct { | |
| ID int64 | |
| Status Status | |
| Conclusion Conclusion | |
| Name string | |
| Steps Steps | |
| StartedAt time.Time `json:"started_at"` | |
| CompletedAt time.Time `json:"completed_at"` | |
| URL string `json:"html_url"` | |
| RunID int64 `json:"run_id"` | |
| } | |
| type Step struct { | |
| Name string | |
| Status Status | |
| Conclusion Conclusion | |
| Number int | |
| Log *zip.File | |
| } |