Skip to content

A GitHub workflow run with a null head_branch or name leaves a permanent "Pending" row with an empty branch and pins its build at High polling priority #400

Open

Description

@matt-edmondson

What's wrong

The GitHub OpenAPI schema marks workflow-run.head_branch and name as nullable. A run from a fork PR whose fork has since been deleted is the usual case. The GitHub provider maps both without a null guard:

  • BuildMonitor/Providers/GitHub.cs:430: gitHubRun.Name.As<RunName>()
  • GitHub.cs:520: run.Branch = gitHubRun.HeadBranch.As<BranchName>();

SemanticString.As<T>() throws ArgumentNullException on null (ktsu.Semantics.Strings 5.12.1). The run is also inserted into build.Runs via GetOrCreate(runId, build.CreateRun(...)) (L431) before UpdateRunFromWorkflowAsync fills it in. So when L520 throws, a Run is already stored with Started set and the default RunStatus.Pending. The exception isn't an ApiException, so MakeGitHubRequestAsync doesn't handle it. SyncGuard logs it and the loop moves on.

The Azure DevOps provider already guards the same field (build.SourceBranch ?? string.Empty).

Failure scenario (reproduced in a scratch MSTest)

UpdateRunFromWorkflowAsync is given a WorkflowRun with "head_branch": null, "status": "completed", "conclusion": "success". The call throws ArgumentNullException and leaves run.Status == Pending, IsOngoing == true, Branch == "", build.HasOngoingRun == true and BuildSync.Priority == High.

In the app:

  • The table shows a row with an empty branch and a progress ring that never completes. The row is persisted, so it survives restarts.
  • Since Poll a build at its running or recent-failure rate whichever branch the run is on [patch] #396, HasOngoingRun keeps the build at High priority. It is polled every 30 s and stays polled in critical-budget mode.
  • Every poll throws at the same run, so the other runs in the 10-run page after it are never updated, and an error is logged each time.
  • Once the run leaves the page, a RunSync is added for the stuck run. GitHubRuns.Get returns the same null and throws, and because the run still reads as ongoing it is retried every 10 s indefinitely.

Suggested fix / acceptance criteria

  • Map HeadBranch ?? string.Empty (or a placeholder such as the head SHA) and Name ?? build.Name.
  • Build and fill the Run completely before adding it to build.Runs, so a mapping exception never leaves a half-initialised Pending run behind.
  • Test: a WorkflowRun with a null head_branch and/or name maps to a completed run without throwing, and no Pending run is left in build.Runs.

Activity

  1. matt-edmondson commented on Nov 5, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Category: Bug
    Priority: High. GitHub sends a null head_branch for any run from a fork PR whose fork has since been deleted, so ordinary repositories hit this. The result lasts: a persisted Pending row with an empty branch, a build pinned at High priority (polled every 30 s, even in critical-budget mode), the other runs in the same 10-run page never updated, and then a RunSync retried every 10 s indefinitely. Every poll spends budget and logs an error.

    Suggested area / assignment: GitHub provider mapping in BuildMonitor/Providers/GitHub.cs (UpdateRunFromWorkflowAsync, L430/L520, and the GetOrCreate at L431). The Azure DevOps provider already guards SourceBranch, so this lines the two providers up.

    Duplicates / in progress: Not a duplicate. It interacts with #396 (closed), which made HasOngoingRun raise priority, and with #332, where ongoing runs with no estimate are polled every 10 s. That interaction is what turns one bad run into constant polling. Open PR #398 (pruning old runs) touches run bookkeeping but does not cover this: a stuck run that still reads as ongoing would not be pruned safely. Fix this first, or check the pruning rule against it.

    Next step: Map HeadBranch ?? string.Empty and Name ?? build.Name, build the Run completely before inserting it into build.Runs, and add the null-field mapping test from the acceptance criteria.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions