From ee5bc5931a39b3d6cfb473905cf6355d21515510 Mon Sep 17 00:00:00 2001 From: beads/crew/emma Date: Wed, 7 Jan 2026 00:36:51 -0800 Subject: [PATCH] feat(gate): auto-discover workflow run ID in gate check (bd-fbkd) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/bd/gate.go | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/cmd/bd/gate.go b/cmd/bd/gate.go index 7c2f7483..f3308301 100644 --- a/cmd/bd/gate.go +++ b/cmd/bd/gate.go @@ -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 --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