gastownhall/beads · error

parse gh output: %w

Error message

parse gh output: %w

What it means

After a successful `gh run list` invocation, its JSON output is unmarshaled into []GHWorkflowRun. If gh's output is not the expected JSON array (empty output, truncated output, human-readable output because --json was dropped/unsupported, or an old gh version with a different schema), this error wraps the json.Unmarshal failure.

Source

Thrown at cmd/bd/gate.go:971

		"--workflow", workflow,
		"--json", "databaseId,name,status,conclusion,createdAt,workflowName",
		"--limit", fmt.Sprintf("%d", limit),
	}
	if repo != "" {
		args = append(args, "--repo", repo)
	}

	output, stderr, err := runGH(args...)
	if err != nil {
		if len(stderr) > 0 {
			return nil, fmt.Errorf("gh run list --workflow=%s failed: %s", workflow, string(stderr))
		}
		return nil, fmt.Errorf("gh run list: %w", err)
	}

	var runs []GHWorkflowRun
	if err := json.Unmarshal(output, &runs); err != nil {
		return nil, fmt.Errorf("parse gh output: %w", err)
	}

	return runs, nil
}

// discoverRunIDByWorkflowName queries GitHub for the most recent run of a workflow.
// Returns (runID, error). This is ZFC-compliant: "most recent run" is deterministic.
func discoverRunIDByWorkflowName(workflowHint string) (string, error) {
	return discoverRunIDByWorkflowNameInRepo(workflowHint, "")
}

func discoverRunIDByWorkflowNameInRepo(workflowHint, repo string) (string, error) {
	return discoverRunIDByWorkflowNameInRepoWithRunner(workflowHint, repo, runGHCommand)
}

// discoverRunIDByWorkflowNameInRepoWithRunner is the runner-injectable form of
// discoverRunIDByWorkflowNameInRepo. checkGHRunWithRunner's cross-repo branch
// must call this (not discoverRunIDByWorkflowNameInRepo directly) so the same

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run `gh run list --workflow=<name> -R <repo> --json ...` manually and confirm valid JSON is printed (pipe to `jq .`).
  2. Upgrade gh CLI to a recent version so the --json field set matches GHWorkflowRun.
  3. If a custom ghCommandRunner is injected, ensure it returns raw gh stdout unmodified (no banners, no reformatting).
  4. Add logging around output length/content to spot truncation or proxy-injected HTML, then fix the transport (proxy, pager: set GH_PAGER= / --no-pager).

Example fix

// before (fake runner returning human output)
return []byte(" Showing 2 of 2 runs"), nil, nil
// after
return []byte(`[{"databaseId":123,"status":"completed","conclusion":"success"}]`), nil, nil
Defensive patterns

Strategy: validation

Validate before calling

// sanity-check gh output before handing it to parsers
out, err := exec.Command("gh", "run", "list", "--workflow=ci.yml", "-R", repo, "--json", "databaseId").Output()
if err == nil {
    var probe []json.RawMessage
    if json.Unmarshal(out, &probe) != nil || len(out) == 0 {
        return errors.New("gh did not return a JSON array; check gh version and --json support")
    }
}

Try / catch

runs, err := queryGitHubRunsForWorkflow("ci.yml", 5)
if err != nil {
    if strings.HasPrefix(err.Error(), "parse gh output:") {
        log.Printf("gh returned unexpected output; upgrade gh or fix runner stdout: %v", err)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: queryGitHubRunsForWorkflowInRepoWithRunner receives malformed/empty stdout from runGH — e.g. gh emitted a warning banner to stdout, a proxy returned HTML, output was truncated, or a fake runner returned non-JSON.

Common situations: Very old gh versions lacking the --json fields used; piping gh through text processors that altered output; capturing human-readable output (missing --json flag) in a custom runner; API degradation returning partial responses.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/9028f8f979cfb4dd. Report an issue: GitHub.