feat(gate): auto-discover workflow run ID in gate check (bd-fbkd)

When bd gate check encounters a gh:run gate with a non-numeric await_id
(workflow name hint like release.yml), it now auto-discovers the run ID:

- Queries GitHub directly with gh run list --workflow=X
- Takes the most recent run (deterministic: newest-first from API)
- Updates the gates await_id and proceeds with status check

This is ZFC-compliant: most recent run of workflow X is purely mechanical,
no heuristic scoring. Agents no longer need to run bd gate discover manually.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/emma
2026-01-07 00:36:51 -08:00
committed by Steve Yegge
parent 812f4e5297
commit ee5bc5931a

View File

@@ -638,7 +638,7 @@ type ghPRStatus struct {
Title string `json:"title"`
}
// isNumericID returns true if the string is a valid numeric ID
// isNumericID returns true if the string contains only digits (a GitHub run ID)
func isNumericID(s string) bool {
if s == "" {
return false
@@ -651,20 +651,80 @@ func isNumericID(s string) bool {
return true
}
// queryGitHubRunsForWorkflow queries recent runs for a specific workflow using gh CLI.
// Returns runs sorted newest-first (GitHub API default).
func queryGitHubRunsForWorkflow(workflow string, limit int) ([]GHWorkflowRun, error) {
if _, err := exec.LookPath("gh"); err != nil {
return nil, fmt.Errorf("gh CLI not found: install from https://cli.github.com")
}
args := []string{
"run", "list",
"--workflow", workflow,
"--json", "databaseId,name,status,conclusion,createdAt,workflowName",
"--limit", fmt.Sprintf("%d", limit),
}
cmd := exec.Command("gh", args...)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("gh run list --workflow=%s failed: %s", workflow, string(exitErr.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) {
// Query GitHub directly for this workflow (efficient, avoids limit issues)
runs, err := queryGitHubRunsForWorkflow(workflowHint, 5)
if err != nil {
return "", fmt.Errorf("failed to query workflow runs: %w", err)
}
if len(runs) == 0 {
return "", fmt.Errorf("no runs found for workflow '%s'", workflowHint)
}
// Take the most recent run (gh returns newest-first)
// This is deterministic: "most recent" is a total ordering by creation time
return fmt.Sprintf("%d", runs[0].DatabaseID), nil
}
// checkGHRun checks a GitHub Actions workflow run gate
func checkGHRun(gate *types.Issue) (resolved, escalated bool, reason string, err error) {
if gate.AwaitID == "" {
return false, false, "no run ID specified - run 'bd gate discover' first", nil
return false, false, "no run ID specified - set await_id or use workflow name hint", nil
}
// Check if AwaitID is a numeric run ID or a workflow name hint
runID := gate.AwaitID
// If await_id is a workflow name hint (non-numeric), auto-discover the run ID
if !isNumericID(gate.AwaitID) {
// Non-numeric AwaitID is a workflow name hint, needs discovery
return false, false, fmt.Sprintf("awaiting discovery (workflow hint: %s) - run 'bd gate discover'", gate.AwaitID), nil
discoveredID, discoverErr := discoverRunIDByWorkflowName(gate.AwaitID)
if discoverErr != nil {
return false, false, fmt.Sprintf("workflow hint '%s': %v", gate.AwaitID, discoverErr), nil
}
// Update the gate with the discovered run ID
if updateErr := updateGateAwaitID(nil, gate.ID, discoveredID); updateErr != nil {
return false, false, "", fmt.Errorf("failed to update gate with discovered run ID: %w", updateErr)
}
runID = discoveredID
}
// Run: gh run view <id> --json status,conclusion,name
cmd := exec.Command("gh", "run", "view", gate.AwaitID, "--json", "status,conclusion,name") // #nosec G204 -- gate.AwaitID is a validated GitHub run ID
cmd := exec.Command("gh", "run", "view", runID, "--json", "status,conclusion,name") // #nosec G204 -- runID is a validated GitHub run ID
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr