Skip to content

Include startedAt and completedAt fields when exporting workflow run job steps information #9769

@andyfeller

Description

@andyfeller

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:

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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementa request to improve CLIgh-runrelating to the gh run command

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions