Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions airbyte/_registry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,22 @@ def parse_changelog_html( # noqa: PLR0914
return versions


def registry_entry_commit_date(entry: dict) -> str | None:
"""Return the YYYY-MM-DD date of a registry entry's git commit timestamp."""
commit_timestamp = entry.get("generated", {}).get("git", {}).get("commit_timestamp")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Null metadata blocks the connector catalog

When releaseDate is absent and generated or generated.git is null, registry_entry_commit_date raises instead of returning no date. One such entry aborts the entire registry load.

Learn more

The registry cache converts every connector entry with _registry_entry_to_connector_metadata. That conversion now calls this helper whenever releaseDate is absent. A JSON null for generated or generated.git bypasses the empty-dict default and raises before the cache is assigned in _get_registry_cache. An optional date field then makes all connector lookups fail.

Example: A local registry contains one connector with "generated": null and no releaseDate. Loading any connector from that registry fails instead of returning metadata with release_date=None.

Recommended fix: Treat null generated and git sections as missing before reading commit_timestamp; retain None as the fallback when no usable timestamp exists.

Suggested change
commit_timestamp = entry.get("generated", {}).get("git", {}).get("commit_timestamp")
commit_timestamp = (entry.get("generated") or {}).get("git") or {}
commit_timestamp = commit_timestamp.get("commit_timestamp")

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confirmed. If an entry has "generated": null or "git": null and no releaseDate, .get(..., {}) returns None and the next .get raises AttributeError. Because that happens inside _get_registry_cache, one bad entry breaks every connector lookup. The fix is (entry.get("generated") or {}).get("git") or {}, plus a test with a null entry. This PR is already merged, so I've asked AJ about a follow-up PR.

if isinstance(commit_timestamp, str):
date_match = re.match(r"(\d{4}-\d{2}-\d{2})", commit_timestamp)
if date_match:
return date_match.group(1)
return None


def fetch_registry_version_date(connector_name: str, version: str) -> str | None:
"""Fetch the release date for a specific version from the registry.

Returns the release date string (YYYY-MM-DD) if found, None otherwise.
"""
try: # noqa: PLR1702
try:
registry_url = "https://connectors.airbyte.com/files/registries/v0/oss_registry.json"
response = requests.get(registry_url, timeout=10)
response.raise_for_status()
Expand All @@ -99,19 +109,18 @@ def fetch_registry_version_date(connector_name: str, version: str) -> str | None
for connector in connector_list:
docker_repo = connector.get("dockerRepository", "")
if docker_repo == f"airbyte/{connector_name}":
if connector.get("dockerImageTag") == version:
current_version_date = registry_entry_commit_date(connector)
if current_version_date:
return current_version_date
Comment on lines +112 to +115

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Explicit current-version release dates are overridden

When releaseDate differs from the current version's commit date, fetch_registry_version_date returns the commit date instead. get_connector_version_history then replaces the changelog date with that value.

Learn more

The connector metadata path already prefers releaseDate over the generated timestamp in _registry_entry_to_connector_metadata. The version-history path instead uses the generated timestamp for the current tag and overwrites the changelog date in get_connector_version_history. The two public results therefore disagree for the same current version whenever those dates differ.

Example: An entry has dockerImageTag="7.2.1", releaseDate="2026-07-12", and generated.git.commit_timestamp="2026-07-10T14:26:59Z". Metadata reports July 12, but version history reports July 10.

Recommended fix: Prefer the entry's nonempty releaseDate before falling back to registry_entry_commit_date for the current version, matching metadata conversion.

Suggested change
if connector.get("dockerImageTag") == version:
current_version_date = registry_entry_commit_date(connector)
if current_version_date:
return current_version_date
if connector.get("dockerImageTag") == version:
current_version_date = connector.get("releaseDate") or registry_entry_commit_date(
connector
)
if current_version_date:
return current_version_date

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confirmed. For the current version, fetch_registry_version_date uses the commit timestamp even when the entry has an explicit releaseDate, while the metadata path prefers releaseDate, so the two disagree when those dates differ. The fix is to use connector.get("releaseDate") or registry_entry_commit_date(connector) there too, plus a test. This PR is already merged, so it would go in the same follow-up as the null-generated fix, pending AJ's OK.


releases = connector.get("releases", {})
release_candidates = releases.get("releaseCandidates", {})

if version in release_candidates:
version_data = release_candidates[version]
generated = version_data.get("generated", {})
git_info = generated.get("git", {})
commit_timestamp = git_info.get("commit_timestamp")

if commit_timestamp:
date_match = re.match(r"(\d{4}-\d{2}-\d{2})", commit_timestamp)
if date_match:
return date_match.group(1)
candidate_date = registry_entry_commit_date(release_candidates[version])
if candidate_date:
return candidate_date

break
else:
Expand Down
8 changes: 6 additions & 2 deletions airbyte/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
from pydantic import BaseModel, Field

from airbyte import exceptions as exc
from airbyte._registry_utils import fetch_registry_version_date, parse_changelog_html
from airbyte._registry_utils import (
fetch_registry_version_date,
parse_changelog_html,
registry_entry_commit_date,
)
from airbyte._util.compat import StrEnum
from airbyte._util.meta import is_docker_installed
from airbyte.constants import AIRBYTE_OFFLINE_MODE
Expand Down Expand Up @@ -224,7 +228,7 @@ def _registry_entry_to_connector_metadata(entry: dict) -> ConnectorMetadata:
release_stage=entry.get("releaseStage"),
source_type=entry.get("sourceType"),
documentation_url=entry.get("documentationUrl"),
release_date=entry.get("releaseDate"),
release_date=entry.get("releaseDate") or registry_entry_commit_date(entry),
github_issue_label=entry.get("githubIssueLabel"),
)

Expand Down
86 changes: 86 additions & 0 deletions tests/unit_tests/test_mcp_connector_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,89 @@ def test_get_connector_info_spec_none_when_registry_has_no_spec() -> None:
assert result.config_spec_jsonschema is None
assert result.manifest_url is not None
connector.install.assert_not_called()


def _registry_entry(**overrides: object) -> dict:
"""Build a minimal registry entry for `source-faker`."""
entry: dict = {
"dockerRepository": "airbyte/source-faker",
"dockerImageTag": "7.2.1",
"sourceDefinitionId": "source-faker-definition",
"name": "Faker",
"tags": [],
"suggestedStreams": {"streams": []},
}
entry.update(overrides)
return entry


def test_registry_entry_release_date_falls_back_to_commit_timestamp() -> None:
"""Entries without `releaseDate` use the version's git commit timestamp date."""
from airbyte.registry import _registry_entry_to_connector_metadata

entry = _registry_entry(
generated={"git": {"commit_timestamp": "2026-07-10T14:26:59-07:00"}},
)
metadata = _registry_entry_to_connector_metadata(entry)

assert metadata.release_date == "2026-07-10"


def test_registry_entry_release_date_prefers_release_date_field() -> None:
"""An explicit `releaseDate` wins over the commit timestamp."""
from airbyte.registry import _registry_entry_to_connector_metadata

entry = _registry_entry(
releaseDate="2026-01-01",
generated={"git": {"commit_timestamp": "2026-07-10T14:26:59-07:00"}},
)
metadata = _registry_entry_to_connector_metadata(entry)

assert metadata.release_date == "2026-01-01"


def test_registry_entry_release_date_none_without_any_date() -> None:
"""Entries with neither `releaseDate` nor a commit timestamp report None."""
from airbyte.registry import _registry_entry_to_connector_metadata

metadata = _registry_entry_to_connector_metadata(_registry_entry())

assert metadata.release_date is None


def _mock_registry_response(entry: dict) -> MagicMock:
response = MagicMock()
response.json.return_value = {"sources": [entry], "destinations": []}
response.raise_for_status = MagicMock()
return response


def test_fetch_registry_version_date_uses_current_version_commit_timestamp() -> None:
"""The connector's own entry supplies the date for its current version."""
from airbyte._registry_utils import fetch_registry_version_date

entry = _registry_entry(
generated={"git": {"commit_timestamp": "2026-07-10T14:26:59-07:00"}},
)
with patch("airbyte._registry_utils.requests.get") as mock_get:
mock_get.return_value = _mock_registry_response(entry)
assert fetch_registry_version_date("source-faker", "7.2.1") == "2026-07-10"


def test_fetch_registry_version_date_reads_release_candidates() -> None:
"""Non-current versions still resolve through `releases.releaseCandidates`."""
from airbyte._registry_utils import fetch_registry_version_date

entry = _registry_entry(
releases={
"releaseCandidates": {
"8.0.0-rc.1": {
"generated": {"git": {"commit_timestamp": "2026-08-01T00:00:00Z"}},
},
},
},
)
with patch("airbyte._registry_utils.requests.get") as mock_get:
mock_get.return_value = _mock_registry_response(entry)
assert fetch_registry_version_date("source-faker", "8.0.0-rc.1") == "2026-08-01"
assert fetch_registry_version_date("source-faker", "0.0.0") is None
Loading