π¦«ποΈ 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
- declare a
DeclaredGithubRepo with homepage: null (repo has no homepage set on github)
declastruct plan -> shows homepage empty-string vs null UPDATE
declastruct apply -> no-op (github stores empty string again)
- 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
π¦«ποΈ dispatch to foreman
title
fix(repo): coerce empty-string homepage/description to null (perpetual UPDATE drift)
description
.what
castToDeclaredGithubRepocasts an empty-stringhomepage(anddescription) from the GitHub API to an empty string, but the natural desired value isnull. The two never converge, so everydeclastruct planon aDeclaredGithubReposhows ahomepageempty-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).root-cause
GitHub's REST API returns
homepageanddescriptionas an empty string for repos with no homepage/description set β NOT null. the?? nullcoalesce only fires on null/undefined, so an empty string stays an empty string.homepage= empty stringhomepage: null(there is no homepage).repro
DeclaredGithubRepowithhomepage: null(repo has no homepage set on github)declastruct plan-> showshomepageempty-string vs null UPDATEdeclastruct apply-> no-op (github stores empty string again)observed on
ahbode/infrastructureprovision/github.repoat 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:
(
|| nullis 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 explicitinput.homepage?.trim() || nullalso handles whitespace-only values..also-consider
whichever coercion you choose, apply it symmetrically on the SET side (
setRepo) if it sendshomepage/description, so a null wish is written consistently and a round-trip get -> cast -> set is stable..impact
DeclaredGithubRepo(always dirty)homepageas an empty string (matching the buggy cast) as a workaround, which is backwards