Skip to content

fix(wizard): component_type 422, existing sbomify.json, edge-case hardening + review fixes#240

Merged
vpetersson merged 6 commits into
sbomify:feat/wizard-and-shared-api-clientfrom
aurangzaib048:feat/wizard-and-shared-api-client
Jun 1, 2026
Merged

fix(wizard): component_type 422, existing sbomify.json, edge-case hardening + review fixes#240
vpetersson merged 6 commits into
sbomify:feat/wizard-and-shared-api-clientfrom
aurangzaib048:feat/wizard-and-shared-api-client

Conversation

@aurangzaib048
Copy link
Copy Markdown
Contributor

Summary

Fixes on top of the wizard + consolidated API client feature, targeting its branch (feat/wizard-and-shared-api-client) directly so the diff is just these 6 commits, not the whole feature. (Supersedes #239, which was opened against master and so duplicated the entire feature / #238.)

These can be merged into #238's branch, or cherry-picked onto it.

Commits

  • dc4b02c fix(wizard): valid component_type, existing sbomify.json, error polish
    • component_type "sbom""bom" — the backend ComponentType enum is {bom, document}; "sbom" returned HTTP 422 on every wizard onboarding and Yocto upload. Adds a client-side VALID_COMPONENT_TYPES guard so a bad literal fails fast instead of as an opaque 422.
    • Wizard now detects and pre-fills an existing repo-root sbomify.json, and apply no longer dead-ends on a hand-authored one (keeps it as-is — the action reads it at run time — and logs the unwritten values so they're recoverable).
    • Readable API validation errors (collapses pydantic 422 list-detail into field: msg); run_command(log_errors=) so cdxgen's expected fallback failures don't spam ERROR; corrected the misleading token-scope warning copy.
  • cee6d02 fix: harden enrichment, dtrack, cargo, chainguard edge caseshas_data() counts cle_release_date; dtrack catches base RequestException; cargo generator gets the check_tool_available guard its siblings have; chainguard reads digest via .get().
  • 55f1a1b ci: add textual to mypy pre-commit deps so the hook passes — the mypy hook's isolated env was missing textual, so it saw the wizard's Textual base classes as Any and failed on every commit (in-venv mypy was already clean). Pinned to ==8.2.7 (the uv.lock version).
  • f29d96d refactor: address comprehensive-review findings — explicit warning when wizard edits aren't written to a hand-authored sbomify.json; log_errors also covers timeout/missing-binary; regression tests for the apply call-site, _load_from_disk error paths, and the chainguard provenance digest path; assorted polish.
  • a2664dd docs(wizard) + c611937 fix(wizard) — Copilot-review follow-ups: corrected stale .bak docstrings to match io.py; surface the unwritten sbomify.json payload in the apply log; document the chainguard .get("digest") continue-on-missing semantics.

Testing

  • Full suite: 2315 passed, 4 skipped.
  • pre-commit run --all-files: ruff, ruff-format, mypy all green.
  • Verified end-to-end via the real Textual TUI and a live config matrix (OIDC/token × trunk/tag/manual × cyclonedx/spdx/both × skip/profile/json_config) against a local sbomify backend.

Not included

The sboms.yml source-SBOM coverage regression from ee4f28d (prod + JS self-SBOM jobs replaced by a ca-certificates wizard test artifact) is out of scope for these fixes and is tracked separately on #238 for the feature author.

Five bugs surfaced by end-to-end wizard testing against a live backend,
plus two follow-ups from adversarial review.

P0  component_type "sbom" is not a valid backend enum value ({bom, document})
    — it 422'd on every wizard onboarding and Yocto upload. Switch the three
    call sites to "bom" and add a VALID_COMPONENT_TYPES guard in
    create_component that fails fast (ValueError) on an unknown literal rather
    than letting it become an opaque server 422.

P1  The wizard ignored a pre-existing repo-root sbomify.json: the config form
    opened blank and apply then refused to overwrite (no wizard sentinel),
    dead-ending the run. Detect it (RepoFacts.has_sbomify_json), pre-fill the
    form from disk, and have apply warn-and-continue on the ownership check
    (the file is kept and read by the action at run time) so the workflow and
    components still get created.

P2  - authenticate: correct the misleading "Token spans N workspaces" copy —
      /workspaces returns all of the user's workspaces regardless of token
      scope (the backend does not filter that endpoint).
    - sbomify_api: collapse pydantic 422 list-detail into readable
      "<field>: <msg>" text (clean_validation_error, module-level) instead of
      a raw dict repr; wired into the client error paths and the upload
      destination.
    - generation: add run_command(log_errors=) so cdxgen's expected fallback
      failures log at DEBUG, not ERROR, on the happy path.

Plan-limit detection keys off the raw string detail (not the cleaned text) so
a 403 validation list can't be misclassified as PlanLimitError.

Regression tests added for each fix; full suite 2307 passed.
Four low-severity robustness/consistency fixes found by a follow-up audit
of the action subsystems (each with a regression test):

- enrichment: has_data() now counts cle_release_date — a source returning
  only a release date was wrongly treated as empty and silently discarded
  (metadata.py).

- dependency-track upload: catch the base requests.RequestException, not just
  ConnectionError/Timeout, so an SSLError/ProxyError returns a clean
  UploadResult.failure_result instead of propagating and crashing the step.

- cyclonedx-cargo generator: add the check_tool_available guard its
  cyclonedx-py / syft siblings already have, so supports() returns False when
  cargo-cyclonedx isn't installed (pip installs) — avoids a spurious ERROR and
  a wasted attempt before the orchestrator falls through.

- chainguard: read manifest/layer "digest" via .get() instead of direct
  indexing, so a malformed/non-spec OCI manifest yields a graceful None rather
  than an uncaught KeyError.

Full suite 2312 passed.
The mypy hook runs in an isolated env (pass_filenames: false,
additional_dependencies only). It omitted textual, so mypy saw the wizard's
Textual base classes (App/Screen/widgets) as Any and emitted 10 spurious
"Class cannot subclass value of type Any" / "Returning Any" errors — the hook
failed on every commit even though `uv run mypy sbomify_action` (textual
installed) reports "Success: no issues found".

Adding textual>=0.85.0 to the hook deps resolves all 10 at the root: no code
changes, no type: ignore. Pre-commit now passes mypy, ruff, and ruff-format.
Follow-ups from the multi-dimensional review of the wizard/hardening changes:

- apply.py: the sbomify.json ownership-skip warning now states explicitly that
  values entered in the wizard were NOT written (the form seeds from a
  hand-authored file, so a silent skip was a data-loss surprise); and document
  why a workflow ownership conflict stays fatal while json_config only warns.

- utils.run_command: log_errors now also covers the timeout and missing-binary
  branches, so a cdxgen timeout on a Python lockfile no longer spams ERROR when
  a later generator succeeds (previously only the non-zero-exit path was gated).

- tests: pin the apply.py component_type="bom" call-site directly; cover
  _load_from_disk's malformed/non-dict error paths (form opens blank, no crash);
  cover the chainguard provenance missing-digest path.

- polish: pin the mypy hook's textual to ==8.2.7 (uv.lock version) for a
  reproducible type view; hoist _load_from_disk's local imports to module top;
  isinstance(... list | tuple) idiom; trailing newline in .pre-commit-config.

Full suite 2315 passed; pre-commit (ruff, ruff-format, mypy) green.
The module docstring said apply overwrites "after .bak backup", but io.py
deliberately writes no .bak (git is the source of truth). Align the contract
text with io.py's actual behavior. Addresses a PR sbomify#239 review comment.
Scoped to the code this fork PR's fix commits touched (the feature-code
comments are tracked on the canonical sbomify#238):

- apply.py: surface the unwritten sbomify.json payload in the apply log when
  the ownership check skips a hand-authored file, so the values entered in the
  wizard are recoverable without re-running it.
- apply.py: correct the apply_plan docstring's stale ".bak backup" claim to
  match io.py (sentinel-guarded overwrite, no .bak; git holds prior versions).
- chainguard.py: comment the .get("digest") continue-on-missing semantics in
  _detect_chainguard_from_provenance (mirrors _resolve_platform_digest).

Full suite 2315 passed; ruff/format/mypy green.
@vpetersson vpetersson merged commit 7b01da1 into sbomify:feat/wizard-and-shared-api-client Jun 1, 2026
9 checks passed
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.

2 participants