Skip to content

fix(bundler): treat an explicitly null catalog field as empty, not "None" - #4532

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/catalog-explicit-null-fields
Open

jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/catalog-explicit-null-fields

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

CatalogSource.from_dict and CatalogEntry.from_dict coerce fields with str(data.get(key, "")).

That default only covers a missing key. A key that is present but null — how YAML spells an empty field (author: with nothing after it), and what a generator emits for an absent value in JSON — yields None, and str(None) is the literal "None".

Reproduction on current main (c173bf1)

CatalogEntry.from_dict({"id": "demo", "name": None, "version": None, ...})
name              = 'None'   <-- literal 'None'
version           = 'None'   <-- literal 'None'
role              = 'None'   <-- literal 'None'
description       = 'None'   <-- literal 'None'
author            = 'None'   <-- literal 'None'
license           = 'None'   <-- literal 'None'
download_url      = 'None'   <-- literal 'None'
sha256            = None     (already guarded)
repository        = None     (already guarded)

The same constructor already guards sha256 and repository against exactly this, so the treatment was internally inconsistent.

For CatalogSource it is more than cosmetic

"None" is truthy, so it defeats the required-field guards and the malformed source is accepted:

CatalogSource.from_dict({"id": None, "url": None, "priority": 5, "install_policy": "install-allowed"}, Scope.PROJECT)
# -> id = 'None', url = 'None'
if not source_id:
    raise BundlerError("A catalog source is missing its 'id'.")   # never fires

A user whose config has an empty id: silently gets a catalog source registered under the name "None", pointing at a URL "None".

Fix

Route both constructors through the existing manifest._text helper, whose docstring already describes this precise failure mode for bundle manifests:

A .get(key, "") default only covers a missing key. A key that is present but null … yields None, and str(None) is the literal "None".

After the fix:

fields still literal 'None': NONE  <-- fixed
null id : refused -> A catalog source is missing its 'id'.
null url: refused -> Catalog source 'ok' is missing its 'url'.
normal source still works: ok https://example.test/catalog.json

Verification

  • Fail-before / pass-after: 9 new-vs-baseline failures with the source reverted to upstream/main → 54 passed with the fix.
  • Parametrized across all seven affected CatalogEntry fields, plus dedicated tests for the null id and null url sources.
  • Scoped regression over tests/contract: 170 passed, up from a clean-main baseline of 161 passed, with zero pre-existing or new failures.
  • No circular import introduced by the models.manifest import (verified by importing models.catalog directly).
  • uvx ruff@0.15.0 check src tests → clean

Behaviour change, disclosed: only inputs that previously produced the literal "None" change. A source with a null id/url now raises the accurate "missing its 'id'/'url'" error instead of being accepted; download_url: null now reads as empty, so it reports "has no download_url" rather than failing later as a non-HTTP(S) URL.


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

🤖 Generated with Claude Code

@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner September 11, 2026 16:07
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Sep 12, 2026
@mnriem
mnriem requested a balanced review from Copilot September 17, 2026 10:44

Copilot AI left a comment

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.

🟡 Changes recommended

Catalog entry IDs remain incorrectly normalized, and the nested version-field change lacks regression coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Normalizes explicit null catalog values to empty strings using _text.

Changes:

  • Fixes null source and catalog-entry parsing.
  • Adds regression tests for affected fields and required source values.
  • Reported verification was reviewed but not rerun.
File summaries
File Description
src/specify_cli/bundler/models/catalog.py Applies null-safe text normalization.
tests/contract/test_catalog_schema.py Adds null-value regression coverage.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/bundles/catalogs.py
Comment thread tests/contract/test_catalog_schema.py

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback and resolve conflicts

@mnriem

mnriem commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Please resolve conflicts

…one"

`CatalogSource.from_dict` and `CatalogEntry.from_dict` coerced fields with
`str(data.get(key, ""))`. That default only covers a *missing* key. A key that
is present but null -- how YAML spells an empty field (`author:` with nothing
after it), and what a generator emits for an absent value in JSON -- yields
`None`, and `str(None)` is the literal `"None"`.

Reproduced on main:

    name              = 'None'
    version           = 'None'
    role              = 'None'
    description       = 'None'
    author            = 'None'
    license           = 'None'
    download_url      = 'None'
    sha256            = None      (already guarded)
    repository        = None      (already guarded)

The same constructor already guards `sha256` and `repository` against exactly
this, so the treatment was internally inconsistent.

For `CatalogSource` it is more than cosmetic: `"None"` is truthy, so it
defeats the required-field guards and a malformed source was ACCEPTED --

    CatalogSource.from_dict({"id": None, "url": None, ...})
    -> id = 'None', url = 'None'

registering a catalog source named "None" pointing at a URL "None".

Routed through the existing `manifest._text` helper, whose docstring already
describes this failure mode for bundle manifests.

Rebased onto current main (files moved in the workflow/bundler restructure).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali force-pushed the fix/catalog-explicit-null-fields branch from db47ee5 to f8f6682 Compare October 5, 2026 16:23
@mnriem
mnriem requested a balanced review from Copilot October 5, 2026 16:54
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

@mnriem Conflicts resolved (f8f6682). The fix moved to src/specify_cli/bundles/catalogs.py, with imports adapted to the new layout (from . import BundlerError, from .manifest import _text). Every hunk (source_id, url, entry_id, name, download_url, requires_speckit_version through _text) is confirmed present, and the tests import from specify_cli.bundles.catalogs.

Verified: 11 new cases fail with the source reverted to main; 57 pass with the fix.

Rebased as a single commit on current main; uvx ruff@0.15.0 check src tests is clean. Any remaining local failures are the pre-existing Windows symlink-privilege tests, which fail identically on unmodified main.

Copilot AI left a comment

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.

Copilot review overview

🟢 Approval recommended

The focused fix is consistent with existing normalization behavior and has comprehensive regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (2)

This branch has not been deployed

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

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants