Skip to content

πŸŽ™οΈ task - fix(repo): coerce empty-string homepage/description to null (perpetual UPDATE drift)Β #61

Description

@ehm-a-beaver

πŸ¦«πŸŽ™οΈ dispatch to foreman

πŸ’§ task enqueued
   β”œβ”€ priority = ?
   β”œβ”€ yieldage = ?
   └─ leverage = ?

title
fix(repo): coerce empty-string homepage/description to null (perpetual UPDATE drift)
description

.what

castToDeclaredGithubRepo casts an empty-string homepage (and description) from the GitHub API to an empty string, but the natural desired value is null. The two never converge, so every declastruct plan on a DeclaredGithubRepo shows a homepage empty-string to null UPDATE, and every apply is a no-op that re-stores the empty string. The resource is never idempotent.

.where

src/domain.operations/repo/castToDeclaredGithubRepo.ts (main / 1.7.0)

description: input.description ?? null,   // line 47
homepage: input.homepage ?? null,        // line 48

.root-cause

GitHub's REST API returns homepage and description as an empty string for repos with no homepage/description set β€” NOT null. the ?? null coalesce only fires on null/undefined, so an empty string stays an empty string.

  • remote casts to homepage = empty string
  • a natural wish declares homepage: null (there is no homepage)
  • empty string is not equal to null, so declastruct decides UPDATE, forever

.repro

  1. declare a DeclaredGithubRepo with homepage: null (repo has no homepage set on github)
  2. declastruct plan -> shows homepage empty-string vs null UPDATE
  3. declastruct apply -> no-op (github stores empty string again)
  4. re-plan -> same UPDATE. never converges.

observed on ahbode/infrastructure provision/github.repo at declastruct-github 1.6.0; confirmed unchanged on main (1.7.0).

.fix

coerce empty string to null in the cast, so remote matches a null wish:

description: input.description || null,   // empty string -> null
homepage: input.homepage || null,         // empty string -> null

(|| null is safe here β€” both fields are free-text where empty string and "unset" are equivalent; there is no meaningful empty-string value to preserve.)

alternatively, if you prefer to keep ?? semantics elsewhere, an explicit input.homepage?.trim() || null also handles whitespace-only values.

.also-consider

whichever coercion you choose, apply it symmetrically on the SET side (setRepo) if it sends homepage/description, so a null wish is written consistently and a round-trip get -> cast -> set is stable.

.impact

  • breaks idempotency guarantee for DeclaredGithubRepo (always dirty)
  • noise in every plan; erodes trust in the KEEP vs UPDATE signal
  • forces consumers to declare homepage as an empty string (matching the buggy cast) as a workaround, which is backwards

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