Repository: cli/cli (go)

Issue:
gh ru with github enterprise does not work
### Describe the bug

Similar bug mentioned https://github.com/cli/cli/issues/3437, but gh run view or list all return a 404. The URL returned seems right based on REST api docs but not getting any response. When comparing the ID `gh run list` doesn't seem to be correct based on the ids from `gh workflow list`

**gh cli version:** `gh version 2.63.2 (2024-12-05)`
**ghe version:** `3.13.4`

### Steps to reproduce the behavior

1. Complete login to the enterprise server with Github CLI
2. Go to a repository directory that uses that server as a remote
3. Run gh run list returns 404

### Expected vs actual behavior

The gh run list prints out list of workflow runs for the repo to choose from

### Logs

```bash
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
* Request at 2024-12-13 00:23:19.723417 -0600 CST m=+0.101249251
* Request to https://{SERVER_URL}/api/graphql
* Request took 281.385ms
⣾* Request at 2024-12-13 00:23:20.040818 -0600 CST m=+0.418510918
* Request to https://{SERVER_URL}/api/v3/repos/{ORG}/{REPO}/actions/runs?per_page=20&exclude_pull_requests=true
⢿* Request took 421.362291ms
⡿* Request at 2024-12-13 00:23:20.534045 -0600 CST m=+0.911535293
* Request to https://{SERVER_URL}/api/v3/repos/{ORG}/{REPO}/actions/workflows?per_page=100&page=1
⣟* Request took 105.218541ms
* Request at 2024-12-13 00:23:20.700194 -0600 CST m=+1.077616418
* Request to https://{SERVER_URL}/api/v3/repos/{ORG}/{REPO}/actions/workflows/63737
⣯* Request took 121.476458ms
failed to get runs: HTTP 404: Not Found (https://{SERVER_URL}/api/v3/repos/{ORG}/{REPO}/actions/workflows/63737)
```


Pull request:
`run list`: do not fail on organization required workflows
Fixes #10076

> [!NOTE]
> There are slight differences between this and the acceptance criteria, see below and please provide feedback.

### Acceptance Criteria

> **Given** I there is a `run` whose workflow is required via an org ruleset
> **When** I run `gh run list`
> **Then** Under the `WORKFLOW` column I see no entry.

I was unsure of what "no entry" means here, but I interpreted it as meaning there should be whitespace in that column. I could also read this as wanting the words "no entry" to appear in the column.

Unit test output:

```
STATUS  TITLE        WORKFLOW  BRANCH  EVENT  ID  ELAPSED  AGE
X       cool commit            trunk   push   1   4m34s    about 4 minutes ago
*       cool commit            trunk   push   2   4m34s    about 4 minutes ago
✓       cool commit            trunk   push   3   4m34s    about 4 minutes ago
X       cool commit            trunk   push   4   4m34s    about 4 minutes ago
X       cool commit  CI        trunk   push   5   4m34s    about 4 minutes ago
-       cool commit  CI        trunk   push   6   4m34s    about 4 minutes ago
-       cool commit  CI        trunk   push   7   4m34s    about 4 minutes ago
*       cool commit  CI        trunk   push   8   4m34s    about 4 minutes ago
*       cool commit  CI        trunk   push   9   4m34s    about 4 minutes ago
```

> **When** I run `gh run list --help`
> **Then** I see an explanation of what no entry means.

```
 ./bin/gh run list --help
List recent workflow runs.

Note that providing the `workflow_name` to the `-w` flag will not fetch disabled workflows.
Also pass the `-a` flag to fetch disabled workflow runs using the `workflow_name` and the `-w` flag.

A run with no workflow name indicates that the run likely belongs an organization ruleset required workflow,
and the authenticated user does not have access to the workflow definition.

For more information about output formatting flags, see `gh help formatting`.
```

> **Given** I there is a `run` whose workflow is required via an org ruleset
> **When** I run `gh run list --json workflowName`
> **Then** I see `nil` for the value of `workflowName`
> I'm choosing `nil` because when programmatically interpreting this, it should not be confused with any other name.

I'm interested in knowing if we would be okay with simply having an empty string instead of `"nil"`:

example `gh run list --json workflowName`
```
<snip>
  {
    "workflowName": ""
  },
  {
    "workflowName": ""
  },
<snip>
```

If we do desire the string `"nil"` in the JSON output, then I'd be interested in hearing some ideas on implementation - how do we get blank whitespaces in the workflow name when running `gh run list` normally, but `"nil"` when exporting it as JSON? 

Seems to me that it would be easier to just keep both cases as an empty string 🤷 

> **When** I run `gh run list --help`
> **Then** I see an explanation that the `workflowName` field may be nil in this case
> 
> **Note**: Need to understand whether we have `workflowDatabaseId` json field. 
> 

Same comment as above.


Patch:
diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go
index 3113fdabd..7b18c391d 100644
--- a/pkg/cmd/run/list/list.go
+++ b/pkg/cmd/run/list/list.go
@@ -62,6 +62,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 
 			Note that providing the %[1]sworkflow_name%[1]s to the %[1]s-w%[1]s flag will not fetch disabled workflows.
 			Also pass the %[1]s-a%[1]s flag to fetch disabled workflow runs using the %[1]sworkflow_name%[1]s and the %[1]s-w%[1]s flag.
+
+			Runs created by organization and enterprise ruleset workflows will not display a workflow name due to GitHub API limitations.
 		`, "`"),
 		Aliases: []string{"ls"},
 		Args:    cobra.NoArgs,
diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go
index 040888ab5..ce909fd77 100644
--- a/pkg/cmd/run/shared/shared.go
+++ b/pkg/cmd/run/shared/shared.go
@@ -446,6 +446,18 @@ func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run)
 		if _, ok := workflowMap[run.WorkflowID]; !ok {
 			// Look up workflow by ID because it may have been deleted
 			workflow, err := workflowShared.GetWorkflow(client, repo, run.WorkflowID)
+			// If the error is an httpError and it is a 404, this is likely a
+			// organization or enterprise ruleset workflow. The user does not
+			// have permissions to view the details of the workflow, so we cannot
+			// look it up directly without receiving a 404, but it is nonetheless
+			// in the workflow run list. To handle this, we set the workflow name
+			// to an empty string.
+			// Deciding to put this here instead of in GetWorkflow to allow
+			// the caller to decide what a 404 means.
+			if httpErr, ok := err.(api.HTTPError); ok && httpErr.StatusCode == 404 {
+				workflowMap[run.WorkflowID] = ""
+				continue
+			}
 			if err != nil {
 				return err
 			}


Test patch:
diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go
index 7717c2ff9..0f11e492a 100644
--- a/pkg/cmd/run/list/list_test.go
+++ b/pkg/cmd/run/list/list_test.go
@@ -366,6 +366,44 @@ func TestListRun(t *testing.T) {
 				completed	stale	cool commit	CI	trunk	push	10	4m34s	2021-02-23T04:51:00Z
 			`),
 		},
+		{
+			name: "org ruleset workflow in runs list shows with empty workflow name",
+			opts: &ListOptions{
+				Limit: defaultLimit,
+				now:   shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34),
+			},
+			isTTY: true,
+			stubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
+					httpmock.JSONResponse(shared.RunsPayload{
+						WorkflowRuns: shared.TestRunsWithOrgRequiredWorkflows,
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"),
+					httpmock.JSONResponse(workflowShared.WorkflowsPayload{
+						Workflows: []workflowShared.Workflow{
+							shared.TestWorkflow,
+						},
+					}))
+				reg.Register(
+					httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/456"),
+					httpmock.StatusStringResponse(404, "not found"),
+				)
+			},
+			wantOut: heredoc.Doc(`
+				STATUS  TITLE        WORKFLOW  BRANCH  EVENT  ID  ELAPSED  AGE
+				X       cool commit            trunk   push   1   4m34s    about 4 minutes ago
+				*       cool commit            trunk   push   2   4m34s    about 4 minutes ago
+				✓       cool commit            trunk   push   3   4m34s    about 4 minutes ago
+				X       cool commit            trunk   push   4   4m34s    about 4 minutes ago
+				X       cool commit  CI        trunk   push   5   4m34s    about 4 minutes ago
+				-       cool commit  CI        trunk   push   6   4m34s    about 4 minutes ago
+				-       cool commit  CI        trunk   push   7   4m34s    about 4 minutes ago
+				*       cool commit  CI        trunk   push   8   4m34s    about 4 minutes ago
+				*       cool commit  CI        trunk   push   9   4m34s    about 4 minutes ago
+			`),
+		},
 		{
 			name: "pagination",
 			opts: &ListOptions{
diff --git a/pkg/cmd/run/shared/test.go b/pkg/cmd/run/shared/test.go
index 7caa37039..0619541a4 100644
--- a/pkg/cmd/run/shared/test.go
+++ b/pkg/cmd/run/shared/test.go
@@ -18,6 +18,10 @@ func TestRunWithCommit(id int64, s Status, c Conclusion, commit string) Run {
 	return TestRunWithWorkflowAndCommit(123, id, s, c, commit)
 }
 
+func TestRunWithOrgRequiredWorkflow(id int64, s Status, c Conclusion, commit string) Run {
+	return TestRunWithWorkflowAndCommit(456, id, s, c, commit)
+}
+
 func TestRunWithWorkflowAndCommit(workflowId, runId int64, s Status, c Conclusion, commit string) Run {
 	return Run{
 		WorkflowID: workflowId,
@@ -57,6 +61,18 @@ var TestRuns []Run = []Run{
 	TestRun(10, Completed, Stale),
 }
 
+var TestRunsWithOrgRequiredWorkflows []Run = []Run{
+	TestRunWithOrgRequiredWorkflow(1, Completed, TimedOut, "cool commit"),
+	TestRunWithOrgRequiredWorkflow(2, InProgress, "", "cool commit"),
+	TestRunWithOrgRequiredWorkflow(3, Completed, Success, "cool commit"),
+	TestRunWithOrgRequiredWorkflow(4, Completed, Cancelled, "cool commit"),
+	TestRun(5, Completed, Failure),
+	TestRun(6, Completed, Neutral),
+	TestRun(7, Completed, Skipped),
+	TestRun(8, Requested, ""),
+	TestRun(9, Queued, ""),
+}
+
 var WorkflowRuns []Run = []Run{
 	TestRun(2, InProgress, ""),
 	SuccessfulRun,