Skip to content

Stop the GitHub directory connector storing absent profile fields as the text "None" - #2415

Merged
mozhaev-dev merged 1 commit into
mainfrom
2393-signin-resolves-wrong-person
Aug 11, 2026
Merged

Stop the GitHub directory connector storing absent profile fields as the text "None"#2415
mozhaev-dev merged 1 commit into
mainfrom
2393-signin-resolves-wrong-person

Conversation

@mozhaev-dev

@mozhaev-dev mozhaev-dev commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #2393.

Problem

The GitHub directory manifest builds four profile fields with a Jinja or none while declaring value_type: string:

value: "{{ (record.get('node') or {}).get('email') or none }}"
value_type: string

A rendered none becomes the four-character text None. So a field the GitHub API does not expose is stored as a value, not as an absence.

For email that is not cosmetic. Identity resolution anchors on e-mail: the seed groups accounts that share an address into one person. GitHub returns null for a member's e-mail unless it is verified and visible to the token's scopes — the common case, not an edge case — so every such member carries the same "None" "address", they group into a single person, and a sign-in through that IdP can resolve to somebody else's profile. That is the failure #2393 describes.

The same or none shape was on name, company and role, so those fields could read as the literal None too.

Why '' and not a real NULL

Deliberate, and the more interesting half of the change.

fields_history detects a change with curr != prev. That comparison is NULL-unsafe in ClickHouse: across a NULL boundary it yields NULL, so no history row is emitted in either direction. Verified:

expression result
NULL != 'None' NULL → no row
'x' != NULL NULL → no row
'' != 'None' true
'x' != '' true

With NULL, a field that later gained a value would never be observed at all — trading a wrong merge for a silent loss. The empty string compares normally, and the downstream filters (new_value != '' in identity_inputs_from_history, WHERE <field> != '' in fields_history) already read it as absence.

Two fields in this same manifest never had the defect and both show the intended shape: login_normalized puts or '' before its filter, and member_id declares no value_type, so its none stays a real null.

Affected areas

  • connectors/git/github-directory/connector.yaml — the four field definitions.
  • connectors/git/github-directory/tests/test_org_members.py — one new case.

Nothing else. No dbt models, no other connector, no backend service. bronze_github_directory.org_members already declares all four columns Nullable(String), so no DDL change is needed, and no relation outside this connector reads these fields.

How to test

cd src/ingestion/tests/connectors
uv run --project . pytest ../../connectors/git/github-directory/tests -q

Expected: 9 passed. The new case (test_absent_profile_fields_are_empty_not_the_text_none) fails on the previous manifest and passes on this one.

The shared member fixture already set email: None but asserted nothing about it, so the case looked covered and was not — that is why this shipped.

What this change does NOT do

Reviewers should know two limits, both structural rather than oversights:

  1. It does not repair rows already written. The persons journal is append-only, so bindings a previous run derived from the "None" address stay in force and continue to read as resolved — they do not fall back into the operator review queue on their own. An environment that already ingested this connector needs a separate data repair.

  2. Correcting the connector does not by itself clear the stale observation. identity_inputs_from_history filters empty values rather than emitting a tombstone, so a "None""" transition produces a history row the macro then drops — nothing supersedes the earlier observation. Clearing it means removing those rows from the identity inputs, not just fixing the source.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • GitHub directory member profiles now show blank values for missing name, email, company, and role fields instead of displaying “None.”
    • Improved consistency for profile history comparisons and filtering of absent values.

…the text "None"

`email`, `name`, `company` and `role` were built with a Jinja `or none` while
carrying `value_type: string`. A rendered `none` becomes the four-character text
`None`, so a field the API does not expose is stored as a VALUE rather than as an
absence.

It matters most for `email`, because identity resolution anchors on it: members
whose address the API does not expose all share one "address", group into a
single person, and a sign-in can then load a different person's profile (#2393).

`''` rather than a real NULL, and deliberately so. `fields_history` detects
changes with `curr != prev`, which is NULL-unsafe in ClickHouse: across a NULL
boundary the comparison yields NULL and NO history row is emitted in EITHER
direction, so a field that later gained a value would never be observed at all —
trading a wrong merge for a silent loss. The empty string compares normally, and
the downstream filters (`new_value != ''`, `WHERE <field> != ''`) already read it
as absence.

Two fields in this manifest never had the defect, and both show the shape:
`login_normalized` puts `or ''` before its filter, and `member_id` carries no
`value_type`, so its `none` stays a real null.

The stream tests set `email: None` in the shared member fixture but asserted
nothing about it, so the case looked covered and was not. It is now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The GitHub directory connector now converts missing organization member profile fields to empty strings. A test verifies normalization for null email, name, and company values.

Changes

GitHub member profile normalization

Layer / File(s) Summary
Normalize absent profile fields
src/ingestion/connectors/git/github-directory/connector.yaml, src/ingestion/connectors/git/github-directory/tests/test_org_members.py
The org_members transformations emit empty strings for missing name, email, company, and role values. Tests verify the output for null profile fields.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: aleksdotbar

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The change reduces one identity-grouping cause but does not ensure incorrect sessions are refused or resolved to the authenticated person. Add or verify authentication safeguards that reject ambiguous mappings and address existing stale identity observations.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change to stop storing absent GitHub profile fields as the text "None".
Out of Scope Changes check ✅ Passed The connector update and regression test are directly related to the linked identity-resolution issue.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2393-signin-resolves-wrong-person

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mozhaev-dev
mozhaev-dev marked this pull request as ready for review August 11, 2026 03:52
@mozhaev-dev
mozhaev-dev requested a review from a team as a code owner August 11, 2026 03:52

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ingestion/connectors/git/github-directory/tests/test_org_members.py`:
- Around line 141-156: Remove the multi-line explanatory docstring from
test_absent_profile_fields_are_empty_not_the_text_none, leaving the test name
and assertions as the sole expression of the expected behavior.
- Around line 160-169: Update the _member fixture in this test to pass
role=None, and include "role" in the fields asserted by the normalization loop.
Preserve the existing expectation that every nullable field is normalized to an
empty string.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 238f03be-3576-44c0-b793-8184574c9e39

📥 Commits

Reviewing files that changed from the base of the PR and between db25b2d and 9640bd8.

📒 Files selected for processing (2)
  • src/ingestion/connectors/git/github-directory/connector.yaml
  • src/ingestion/connectors/git/github-directory/tests/test_org_members.py

Comment on lines +141 to +156
"""A field GitHub does not expose must arrive as `''`, never as `"None"`.

GitHub returns `null` for a member's e-mail unless it is verified and
visible to the token's scopes, so this is the COMMON case, not an edge one.
These fields carry `value_type: string`, which renders a Jinja `none` as the
four-character text `None` — a value, not an absence. Identity anchors on
e-mail, so every such member would share one "address" and be grouped into a
single person: one member's sign-in would then load another's profile.

Asserted as `''` rather than as a real NULL deliberately.
`fields_history` detects changes with `curr != prev`, which yields NULL
across a NULL boundary in ClickHouse and emits NO history row in either
direction — so a field that later gained a value would never be observed at
all. `''` compares normally and the downstream filters already read it as
absence.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the explanatory docstring.

test_absent_profile_fields_are_empty_not_the_text_none already states the behavioral rule. The docstring repeats the rule and implementation details.

As per coding guidelines, “Test non-obvious semantics with a test whose name states the rule rather than adding a comment,” and “Add comments only when code cannot express the reason ... keep them to one line.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ingestion/connectors/git/github-directory/tests/test_org_members.py`
around lines 141 - 156, Remove the multi-line explanatory docstring from
test_absent_profile_fields_are_empty_not_the_text_none, leaving the test name
and assertions as the sole expression of the expected behavior.

Source: Coding guidelines

Comment on lines +160 to +169
_page([_member("dev-one", 7001, email=None, name=None, company=None)]),
)

record = read_stream(CONNECTOR, _STREAM, config).records[0].record.data

for field in ("email", "name", "company"):
assert record[field] == "", (
f"{field} came through as {record[field]!r}; anything other than '' "
f"is stored as a value and groups unrelated members together"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test null role normalization.

The connector normalizes role to "", but this fixture keeps role="MEMBER" and the assertion loop omits role. Pass role=None and include role in the asserted fields.

Proposed test update
-def _member(login: str, database_id: int, role: str = "MEMBER", **node) -> dict:
+def _member(login: str, database_id: int, role: str | None = "MEMBER", **node) -> dict:
 ...
-        _page([_member("dev-one", 7001, email=None, name=None, company=None)]),
+        _page(
+            [_member("dev-one", 7001, role=None, email=None, name=None, company=None)]
+        ),
 ...
-    for field in ("email", "name", "company"):
+    for field in ("email", "name", "company", "role"):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ingestion/connectors/git/github-directory/tests/test_org_members.py`
around lines 160 - 169, Update the _member fixture in this test to pass
role=None, and include "role" in the fields asserted by the normalization loop.
Preserve the existing expectation that every nullable field is normalized to an
empty string.

@mozhaev-dev
mozhaev-dev added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 5517d7d Aug 11, 2026
60 of 61 checks passed
@mozhaev-dev
mozhaev-dev deleted the 2393-signin-resolves-wrong-person branch August 11, 2026 04:55
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.

After sign-in the portal shows a different person's profile and data

3 participants