Skip to content

[codex] Handle unreported runner completion#47

Merged
intel352 merged 1 commit into
mainfrom
codex/runner-blank-name-completion-20260706
Jul 6, 2026
Merged

[codex] Handle unreported runner completion#47
intel352 merged 1 commit into
mainfrom
codex/runner-blank-name-completion-20260706

Conversation

@intel352

@intel352 intel352 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Treat a unique post-dispatch terminal GitHub workflow job with blank runner_name as completed evidence instead of polling forever.
  • Keep the fallback fail-closed when multiple post-dispatch runs are returned and ignore skipped terminal jobs.
  • Add regression coverage for the live STG shape where the target run logs show the ephemeral runner name, but the jobs API omits runner_name.

Root Cause

The workflow-compute STG dogfood run dispatched and completed the GitHub target workflow, but the provider parent stayed in its STG proof wait. Diagnostics showed the retained agent emitted github-runner-proof.json, while GitHub jobs API data available to the provider can omit runner_name for the completed ephemeral self-hosted job. The provider only accepted exact runner_name matches, so it kept polling after success.

Self-Review

Adversarial review flagged a false-positive risk if multiple post-dispatch runs are returned. The fallback now only applies when the post-dispatch run set is unique; otherwise it fails closed and continues polling/timeout instead of accepting ambiguous evidence.

Verification

  • RED, fix removed: GOWORK=off go test ./cmd/github-actions-runner-job -run TestT915WaitForGitHubCompletionAcceptsBlankRunnerNameAfterTerminalSuccess -count=1 failed with waitForGitHubCompletion blocked when GitHub job API omitted runner_name.
  • RED, ambiguity guard missing: GOWORK=off go test ./cmd/github-actions-runner-job -run TestT915ObserveGitHubJobDoesNotUseBlankRunnerFallbackAcrossMultipleRuns -count=1 failed by accepting the first blank-runner run.
  • GREEN: GOWORK=off go test ./cmd/github-actions-runner-job -run 'TestT915(WaitForGitHubCompletion(AcceptsBlankRunnerNameAfterTerminalSuccess|DoesNotBlockOnRunnerShutdownAfterTerminalSuccess|DoesNotBlockOnRunnerShutdownAfterTimeout)|CommandTreatsGitHubJobAPIAsCompletion|ObserveGitHubJobDoesNotUseBlankRunnerFallbackAcrossMultipleRuns)' -count=1
  • GOWORK=off go test ./... -count=1
  • GOWORK=off go build ./cmd/github-runner-provider ./cmd/github-actions-runner-job
  • git diff --check

Copilot AI review requested due to automatic review settings July 6, 2026 11:45
@intel352
intel352 merged commit 0606967 into main Jul 6, 2026
6 checks passed
@intel352
intel352 deleted the codex/runner-blank-name-completion-20260706 branch July 6, 2026 11:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the GitHub Actions runner-job observer to treat a single, terminal workflow job with an omitted/blank runner_name as completion evidence (instead of polling indefinitely), and adds regression tests for the STG-shaped API response that triggered the issue.

Changes:

  • Add a “blank runner name” terminal fallback in observeGitHubJob when the post-dispatch run set is unique and the job is terminal (skipping skipped conclusions).
  • Keep ambiguity fail-closed when multiple post-dispatch runs are returned (do not apply the fallback).
  • Add regression tests covering the blank-runner_name terminal success case and the multi-run ambiguity guard.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
cmd/github-actions-runner-job/main.go Adds terminal completion fallback logic when runner_name is blank, to avoid indefinite polling after successful dispatch.
cmd/github-actions-runner-job/main_test.go Adds regression tests for the blank-runner_name terminal success scenario and multi-run ambiguity guard.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 494 to 497
var unassigned githubJobCompletion
var terminalUnreported githubJobCompletion
allowTerminalUnreported := len(runs) == 1
for _, run := range runs {
Comment on lines +515 to +534
if allowTerminalUnreported && jobRunner == "" && terminalUnreported.WorkflowRunID == 0 && (runTerminal || jobTerminal) {
conclusion := strings.ToLower(strings.TrimSpace(job.Conclusion))
if conclusion == "" {
conclusion = strings.ToLower(strings.TrimSpace(run.Conclusion))
}
if conclusion == "skipped" {
continue
}
completion := githubJobCompletion{
WorkflowRunID: run.ID,
WorkflowJobID: job.ID,
WorkflowJobStatus: job.Status,
Assigned: true,
Terminal: true,
Message: fmt.Sprintf("run=%d job=%d status=%s conclusion=%s runner=unreported", run.ID, job.ID, job.Status, job.Conclusion),
}
completion.Success = conclusion == "success" || conclusion == "succeeded"
terminalUnreported = completion
continue
}
Comment on lines +557 to +559
if terminalUnreported.WorkflowRunID != 0 {
return terminalUnreported, nil
}
Comment on lines +500 to +539
func TestT915ObserveGitHubJobDoesNotUseBlankRunnerFallbackAcrossMultipleRuns(t *testing.T) {
sidecar := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer provider-token" {
t.Fatalf("authorization = %q", got)
}
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/actions/repos/GoCodeAlone/workflow-compute/workflows/dogfood.yml/runs":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"workflow_runs":[{"id":28788166953,"status":"completed","conclusion":"success"},{"id":28788166954,"status":"completed","conclusion":"success"}]}`))
case r.Method == http.MethodGet && r.URL.Path == "/v1/actions/repos/GoCodeAlone/workflow-compute/actions/runs/28788166953/jobs":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"jobs":[{"id":85359812345,"run_id":28788166953,"status":"completed","conclusion":"success","runner_name":""}]}`))
case r.Method == http.MethodGet && r.URL.Path == "/v1/actions/repos/GoCodeAlone/workflow-compute/actions/runs/28788166954/jobs":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"jobs":[{"id":85359812346,"run_id":28788166954,"status":"completed","conclusion":"success","runner_name":""}]}`))
default:
t.Fatalf("unexpected sidecar request: %s %s", r.Method, r.URL.String())
}
}))
t.Cleanup(sidecar.Close)

driver := &runnerDriver{
req: internal.EphemeralRunnerJobRequest{
Repository: "GoCodeAlone/workflow-compute",
Workflow: "dogfood.yml",
},
sidecar: &providerSidecarClient{
baseURL: sidecar.URL,
token: "provider-token",
http: sidecar.Client(),
},
}
completion, err := driver.observeGitHubJob(context.Background(), "wfc-stg-ghp-linux-260629fabb6f-12640z85f6ed", time.Now().UTC().Add(-time.Minute))
if err != nil {
t.Fatalf("observe GitHub job: %v", err)
}
if completion.WorkflowRunID != 0 {
t.Fatalf("blank runner fallback must fail closed across multiple runs, got %+v", completion)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants