Skip to content

[Bug][github_graphql] PR/issue author is empty for Bot actors (GitHub Apps, Dependabot) — GraphQL query only spreads "... on User" #9085

Description

@zyv4yk

Search before asking

  • I had searched in the issues and found no similar issues.

What happened

When the GitHub connection has Use GraphQL APIs enabled, every pull request, issue and review opened by an actor of GraphQL type Bot (GitHub Apps, Dependabot, Renovate, GitHub Actions, any AI code-review or automation app) is stored with an empty author: author_name = '' and author_id = 0 in _tool_github_pull_requests, and no pull_requests.author_id in the domain layer.

The data is already empty in the raw layer, so it cannot be recovered by re-running the extractor:

"Author": {"Login": "", "Id": 0, "Name": "", "Company": "", "Email": "", ...}

Root cause is in backend/plugins/github_graphql/tasks/account_graphql_pre_extractor.go:

type GraphqlInlineAccountQuery struct {
	GithubAccountEdge `graphql:"... on User"`
}

func extractGraphqlPreAccount(result *[]interface{}, res *GraphqlInlineAccountQuery, repoId int, connId uint64) {
	if res == nil || res.Id == 0 {
		return
	}

author, mergedBy, assignees and review authors are all typed as Actor in the GitHub schema. Actor is implemented by User, Bot, Organization, Mannequin and EnterpriseUserAccount. The query spreads only ... on User, so for any non-User actor GitHub returns an empty selection set, and the res.Id == 0 guard then drops the account entirely.

The REST collector is not affected — it resolves bot authors correctly — which is why a connection using GraphQL shows almost no bot-authored PRs while a REST one does.

What do you expect to happen

PRs, issues and reviews opened by GitHub Apps / bots should carry their author, with the same identity the REST collector produces (dependabot[bot], id 49699333), so that accounts rows, pull_requests.author_id and any bot-aware metric work the same regardless of which collector is used.

How to reproduce

The behaviour can be shown against the GitHub API directly, no DevLake instance needed. This query mirrors what the plugin sends today (userFragmentOnly) next to what it should send (withBotFragment):

gh api graphql -f query='
{
  search(query: "repo:grafana/grafana is:pr author:app/dependabot", type: ISSUE, first: 2) {
    nodes {
      ... on PullRequest {
        number
        userFragmentOnly: author { ... on User { login databaseId } }
        withBotFragment:  author { __typename ... on User { login databaseId } ... on Bot { login databaseId } }
      }
    }
  }
}'

Result:

{"number":131544,
 "userFragmentOnly":{},
 "withBotFragment":{"__typename":"Bot","login":"dependabot","databaseId":49699333}}

End to end:

  1. Create a GitHub connection with Use GraphQL APIs enabled, scoped to a repo that receives Dependabot/Renovate/GitHub App PRs.
  2. Run the blueprint.
  3. SELECT number, author_name, author_id FROM _tool_github_pull_requests WHERE author_name = ''; — every bot-authored PR is listed.
  4. Repeat with Use GraphQL APIs disabled: the same PRs come back with dependabot[bot] / 49699333.

Anything else

Happens every time, for every bot-authored PR, on every repo. In our deployment roughly 1,000 PRs opened by our own GitHub App over the last eight months are unattributed, and all 18 bot accounts in _tool_github_accounts (collected from comments and reviews) have zero PRs linked to them.

Three details worth knowing before writing the fix:

1. ... on Bot may only be spread where the schema says Actor. GraphqlInlineAccountQuery is also used for fields typed User — PR and issue assignees, and commit.author.user — and spreading the fragment there makes GitHub reject the entire query, so PR and issue collection would stop working:

gh api graphql -f query='
{ repository(owner:"grafana", name:"grafana") { pullRequest(number:131544) {
  assignees(first:1){ nodes { ... on User { login } ... on Bot { login } } } } } }'

{"errors":[{"extensions":{"code":"cannotSpreadFragment","typeName":"Bot","parentName":"User"},
  "message":"Fragment on Bot can't be spread inside User"}]}

The Actor fields need their own type, leaving the existing one User-only:

type GithubBotEdge struct {
	Login     string
	Id        int `graphql:"databaseId"`
	AvatarUrl string
	HtmlUrl   string `graphql:"url"`
}

// for PullRequest.author, PullRequest.mergedBy, PullRequestReview.author, Issue.author
type GraphqlInlineActorQuery struct {
	GithubAccountEdge `graphql:"... on User"`
	Bot               GithubBotEdge `graphql:"... on Bot"`
}

The named-field-with-inline-fragment-tag pattern is already used in this codebase for RequestedReviewer (#7716), so the client handles it.

2. Bot has a different field set than User. It exposes login, databaseId, avatarUrl, url, createdAt, updatedAt — but no name, company or email, hence the separate struct above. Leaving name, company and email empty matches what the REST collector stores for a bot: GET /users/dependabot[bot] returns null for all three.

3. Bot.login omits the [bot] suffix. GraphQL returns dependabot, REST returns dependabot[bot]; the databaseId is the same 49699333 in both, so there is no risk of duplicate accounts, but the logins would diverge between collectors.

This matters for the is_bot flag added in #9000: the fallback is strings.HasSuffix(githubUser.Login, "[bot]"), and _tool_github_accounts.type is never populated on the GraphQL path (GithubAccountEdge has no Type field, and extractGraphqlPreAccount only writes GithubRepoAccount). So a naive fix would produce bot accounts that is_bot cannot detect. The normalisation should append [bot] to match REST, and ideally set Type: "Bot" on the GithubAccount row.

It also interacts with #8886 / #8894: pr_convertor.go now guards if pr.AuthorId != 0 before generating the account id, so as long as the author stays empty these PRs will keep having no author_id by design — the fix has to happen at collection time.

Existing data cannot be repaired by re-running extract or convert; a Full Refresh re-collect is required, since the author is already empty in _raw_github_graphql_prs.

Same fix applies to mergedBy, which is currently empty for merges performed by a bot.

Version

Verified on main @ 79ef9f4f; the code is unchanged since #8583 (2025-09-24) and is present in v1.0.3-beta16.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions