Skip to content
Merged
60 changes: 38 additions & 22 deletions repo-config/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,45 @@ main_ruleset="$script_dir/main.json"
settings_file="$script_dir/settings.json"

# ----- Resolve the declared description (optional, shared by apply and check) -----
# Per GOVERNANCE.md "Repository Details", once a repo declares registry/repos.json's `description` field, that field becomes the About panel's source rather than the README.
# The audit's description_findings() (spec/audit.py) measures the README, About, and Docker Hub mirror set against that same field.
# A repo with no declared field is left untouched here, so the README stays its source of truth.
# Absence keeps the About panel following the README.
description=""
if [ -f "$registry" ]; then
# Trimmed defensively even though spec/validate.py already rejects an untrimmed value.
# A registry edited ahead of its next validate.py run still resolves to the same canonical value spec/audit.py compares against.
if ! description="$(jq -r --arg n "$name" \
'(.repos[] | select(.name==$n) | .description) // "" | gsub("^\\s+|\\s+$"; "")' "$registry")"; then
echo "Failed to read description from $registry (invalid JSON?)." >&2
# Fails loud on a duplicate name (already a validate.py DEFECT) rather than picking one entry over the other.
if ! match_count="$(jq -r --arg n "$name" '[.repos[] | select(.name==$n)] | length' "$registry")"; then
echo "Failed to read $registry (invalid JSON?)." >&2
exit 1
fi
# The trim above only strips leading/trailing whitespace, so an embedded newline or carriage return survives it.
# Caught here rather than left to reach `gh api` as a multi-line value.
case "$description" in
*$'\n'* | *$'\r'*)
echo "The declared description for $name in $registry carries an embedded newline. Fix it there (spec/validate.py rejects this once run)." >&2
if [ "$match_count" -gt 1 ]; then
echo "$match_count registry entries named $name in $registry. Resolve the duplicate before its description can be read (spec/validate.py rejects this once run)." >&2
exit 1
fi
Comment thread
ptr727 marked this conversation as resolved.
if ! declared="$(jq -r --arg n "$name" '.repos[] | select(.name==$n) | has("description")' "$registry")"; then
echo "Failed to read $registry (invalid JSON?)." >&2
exit 1
fi
if [ "$declared" = "true" ]; then
# Exactly one match is already established above, so select() itself yields exactly one value here.
# Trims only space/tab, so an edge newline survives to trip the guard below rather than being silently dropped.
# A non-string value (including an explicit null) resolves to empty here, caught by the same guard as a malformed one.
# -j plus the trailing sentinel keeps command substitution from stripping a genuine trailing newline in the value.
if ! description="$(jq -j --arg n "$name" \
'(.repos[] | select(.name==$n) | .description) | if type == "string" then gsub("^[ \\t]+|[ \\t]+$"; "") else empty end' \
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"$registry" && printf x)"; then
Comment thread
ptr727 marked this conversation as resolved.
echo "Failed to read description from $registry (invalid JSON?)." >&2
exit 1
;;
esac
fi
description="${description%x}"
if [ -z "$description" ]; then
echo "The declared description for $name in $registry is not a non-empty string. Fix it there (spec/validate.py rejects this once run)." >&2
exit 1
fi
case "$description" in
*$'\n'* | *$'\r'*)
echo "The declared description for $name in $registry carries an embedded newline. Fix it there (spec/validate.py rejects this once run)." >&2
exit 1
;;
esac
fi
fi

# ----- Ruleset id lookup (shared by apply and check) -----
Expand Down Expand Up @@ -181,8 +200,7 @@ cmd_apply() {
payload="$(jq --argjson d "$disc" '. + {has_discussions: $d}' "$settings_file")"
echo "Warning: $repo has no 'main' branch. Leaving default_branch unchanged." >&2
fi
# The About description, only once a repo declares registry/repos.json's `description` (see the resolution above).
# Left untouched otherwise, so a repo that has not adopted the field yet keeps its hand-set (or README-derived) description.
# Applies only once a repo declares the field (see the resolution above).
if [ -n "$description" ]; then
payload="$(jq --arg desc "$description" '. + {description: $desc}' <<<"$payload")"
fi
Expand Down Expand Up @@ -303,15 +321,13 @@ check_settings() {
if gh api "repos/$repo/branches/main" --jq '.name' >/dev/null 2>&1; then
assert "default_branch = main" test "$(jq -r '.default_branch' <<<"$live")" = main
fi
# The About description, only where the registry declares one (see the resolution above).
# A repo that has not adopted the field is a manual-verify note, exactly as secrets are: nothing declared here to check against.
# The two reasons `$description` can be empty are told apart, since "no registry" and "no field for this repo" call for different follow-up.
# $description is empty for two different reasons, told apart below since each needs different follow-up.
if [ -n "$description" ]; then
assert "description = '$description'" test "$(jq -r '.description' <<<"$live")" = "$description"
elif [ ! -f "$registry" ]; then
note "description: no $registry to read (pass a plain repo argument or run from a hub checkout) - verify manually"
note "description: no $registry to read (it resolves relative to this script, not from the repo argument). Run from a hub checkout for it to exist, and verify manually."
else
note "description: no registry/repos.json description declared for $name - verify manually (falls back to the README tagline, see GOVERNANCE.md 'Repository Details')"
note "description: no registry/repos.json description declared for $name (falls back to the README tagline, see GOVERNANCE.md 'Repository Details'). Verify manually."
fi
}

Expand Down
22 changes: 22 additions & 0 deletions scripts/tests/test_spec_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,28 @@ def test_whitespace_only_is_rejected(self) -> None:
["Fixture: description must be a non-empty string"],
)

def test_an_explicit_null_is_rejected_rather_than_read_as_absent(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", None),
["Fixture: description must be a non-empty string"],
)

def test_for_repo_an_absent_key_produces_no_errors(self) -> None:
self.assertEqual(validate.description_errors_for_repo({}, "Fixture"), [])

def test_for_repo_an_explicit_null_is_rejected_rather_than_read_as_absent(self) -> None:
# Locks in the presence-vs-None guard: this regresses to `[]` if it is ever weakened back to `is not None`.
self.assertEqual(
validate.description_errors_for_repo({"description": None}, "Fixture"),
["Fixture: description must be a non-empty string"],
)

def test_a_non_string_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", 42),
["Fixture: description must be a non-empty string"],
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def test_an_inline_markdown_link_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", "See [docs](https://example.test) for more."),
Expand Down
49 changes: 48 additions & 1 deletion spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,24 @@ def description_findings(doc_texts, entry, live, slug):
declared value is canonical on its own and does not need the README to establish it.
"""
findings = []
declared = (entry.get("description") or "").strip() or None
declared = None
if "description" in entry:
raw_declared = entry["description"]
if isinstance(raw_declared, str) and raw_declared.strip():
declared = raw_declared.strip()
else:
if raw_declared is None:
problem = "is null"
elif isinstance(raw_declared, str):
problem = "is an empty or whitespace-only string"
else:
problem = f"is {type(raw_declared).__name__}, not a string"
findings.append(
(
"DEFECT",
f"registry: description {problem}. spec/validate.py rejects this once run, so this is treated as undeclared.",
)
)
readme_want = None
if "README.md" in doc_texts:
title, intro = title_and_intro(doc_texts["README.md"])
Expand Down Expand Up @@ -4214,6 +4231,27 @@ def _selftest():
{"description": "Anything."},
0,
),
(
"a non-string declared field is reported rather than crashing",
desc_readme,
{"description": 42},
{"description": "A short tagline."},
1,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
(
"an explicit null declared field is reported rather than read as absent",
desc_readme,
{"description": None},
{"description": "A short tagline."},
1,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
(
"a whitespace-only declared field is reported rather than read as absent",
desc_readme,
{"description": " "},
{"description": "A short tagline."},
1,
),
]
for label, doc_texts_fx, entry_fx, live_fx, wantn in desc_cases:
got = description_findings(doc_texts_fx, entry_fx, live_fx, "owner/Fixture")
Expand All @@ -4225,6 +4263,15 @@ def _selftest():
if len(got) != wantn:
for _, t in got:
print(f" {t}")
# A null declared field is a DEFECT naming its actual type, not a silent LETTER-only "About mismatch".
null_declared = description_findings(
desc_readme, {"description": None}, {"description": "A short tagline."}, "owner/Fixture"
)
if not any(k == "DEFECT" and "description is null" in t for k, t in null_declared):
ok = False
print(f" FAIL description: null-declared-field DEFECT contract -> {null_declared}")
else:
print(" ok description: a null declared field is a DEFECT naming its actual type")
# The declared field, once present, is what the wording names as the source - not "the README".
declared_mismatch = description_findings(
desc_readme,
Expand Down
15 changes: 12 additions & 3 deletions spec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ def is_str_list(v):
return isinstance(v, list) and all(isinstance(x, str) for x in v)


def description_errors_for_repo(repo, name):
"""The per-repo optional-field guard: an explicit `"description": null` is declared-but-invalid, not absent.

Presence (`"description" in repo`) is the test, not `repo.get("description") is not None`, so a `null` reaches
description_errors() rather than being read as though the field were never declared.
"""
if "description" not in repo:
return []
return description_errors(name, repo["description"])


def description_errors(name, desc):
"""Shape errors for a registry entry's optional `description` (GOVERNANCE.md "Repository Details").

Expand Down Expand Up @@ -503,9 +514,7 @@ def check_secret_set(label, entry, need_kind):
if effective_model == "operational" and eol is None:
errors.append(f"{name}: operational repo must declare lineEndings (lf or crlf)")
# Optional per GOVERNANCE.md "Repository Details": a repo that has not adopted the field yet is unaffected, since spec/audit.py's description_findings() falls back to the README tagline for it.
desc = repo.get("description")
if desc is not None:
errors.extend(description_errors(name, desc))
errors.extend(description_errors_for_repo(repo, name))

status = repo.get("status")
if status is None:
Expand Down