Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ Contributors commit to this repo with signed commits; the SSH-signing setup live

Every repo's GitHub repository details (the About panel) follow a fixed convention so the fleet stays consistent and self-describing.

- **Description** matches the README's first non-empty line after the `#` H1 heading, as plain text - strip markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. The README is the source of truth: set the description from it (`gh api -X PATCH repos/<owner>/<repo> -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail - the fix is to sharpen the README so the description follows it.
- **Description** matches the README's first non-empty line after the `#` H1 heading, as plain text - strip markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. The README is the source of truth: set the description from it (`gh api -X PATCH repos/<owner>/<repo> -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail - the fix is to sharpen the README so the description follows it. Keep the line at most **100 characters** - Docker Hub's short-description cap, the tightest surface it feeds. For a repo that publishes a Docker image, the **Docker Hub short description** mirrors the same README intro line, so one canonical sentence carries to the README, the About panel, and Docker Hub alike.
- **Topics** are optional; any that are present match the repo's actual content. Do not invent topics to fill the field.
- **Include in the home page**: Releases on, Deployments off, Packages off. These toggles are UI-only - the REST and GraphQL APIs neither read nor write them - so they are set by hand and cannot be audited through `gh`.

Expand Down
54 changes: 48 additions & 6 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import re
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Any

Expand Down Expand Up @@ -74,6 +76,25 @@ def gh(path, ok404=False) -> Any:
return json.loads(r.stdout) if r.stdout.strip() else None


def docker_hub_description(slug):
"""The Docker Hub short description for a repo, or None if the image is genuinely absent (HTTP 404).

The image name is taken as owner/repo lowercased, the fleet convention (`ptr727/PhotoCleaner` ->
`ptr727/photocleaner`), so a repo whose image is named otherwise, or not yet pushed, 404s and is skipped
rather than falsely flagged. A transient failure (timeout, network, non-404 status) is **raised**, not
swallowed, so the caller surfaces "could not verify" instead of silently passing. Read-only, unauthenticated.
"""
owner, repo = slug.split("/", 1)
url = f"https://hub.docker.com/v2/repositories/{owner.lower()}/{repo.lower()}/"
try:
with urllib.request.urlopen(url, timeout=5) as r:
return json.loads(r.read().decode("utf-8")).get("description")
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise


def normalize_ruleset(payload):
sub = {k: payload.get(k) for k in RULESET_SUBSET}
if isinstance(sub.get("rules"), list):
Expand Down Expand Up @@ -692,21 +713,42 @@ def audit_repo(entry, spec):
elif r_intro != h_intro:
findings.append(("LETTER", "history: HISTORY.md intro does not mirror the README intro - copy the README's opening paragraph (spec/readme-structure.md)"))

# --- Repository description mirrors the README intro line ---
# AGENTS.md "Repository Details": the About description is the README's first line after the H1 as plain
# text (links stripped), and the README is the source of truth. spec/readme-structure.md additionally wants
# that line link-free, so it carries to the unrendered description without formatting loss.
# --- README title/intro is the one canonical short description ---
# spec/readme-structure.md item 1 + AGENTS.md "Repository Details": the H1 is the repo name, and the intro
# line after it is a link-free, <=100-char plain sentence that carries verbatim to the GitHub About
# description and (for a docker repo) the Docker Hub short description. The README is the source of truth.
if "README.md" in doc_texts:
intro_line = title_and_intro(doc_texts["README.md"])[1].split("\n")[0]
title, intro = title_and_intro(doc_texts["README.md"])
intro_line = intro.split("\n")[0]
# The H1 is the repository name, and a hyphenated name may render its hyphens as spaces.
# Use the GitHub API's canonical name, since the registry-URL slug can carry a different case.
repo_name = live.get("name") or slug.split("/")[-1]
if not title:
findings.append(("LETTER", "readme: no `# ` H1 title - the README opens with `# <repo name>` then a one-line description (spec/readme-structure.md)"))
elif title.replace("-", " ") != repo_name.replace("-", " "):
findings.append(("LETTER", f"readme: the H1 title '{title}' is not the repo name '{repo_name}' (a hyphenated name may render its hyphens as spaces) - the H1 is the repository name (spec/readme-structure.md)"))
if not intro_line:
findings.append(("LETTER", "readme: no intro line after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)"))
else:
if strip_md_links(intro_line) != intro_line:
findings.append(("LETTER", "readme: the intro line carries markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)"))
desc = (live.get("description") or "").strip()
want = strip_md_links(intro_line).strip()
if len(want) > 100:
findings.append(("LETTER", f"readme: the intro line is {len(want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)"))
desc = (live.get("description") or "").strip()
if desc != want:
findings.append(("LETTER", f"description: the About description does not match the README intro line (description '{desc}' vs readme '{want}') - set it from the README, or sharpen the README first if the description carries real detail (AGENTS.md Repository Details)"))
# Docker Hub short description mirrors the same intro, for a repo that publishes a docker image.
# A transient lookup failure surfaces as a DRIFT ("could not verify"), never aborting or silently passing.
# A 404 (image not at the derived name) returns None and is skipped.
if any((pt.get("target") if isinstance(pt, dict) else pt) == "docker" for pt in entry.get("publish", [])):
try:
dh = docker_hub_description(slug)
except Exception as e:
dh = None
findings.append(("DRIFT", f"description: could not read the Docker Hub short description to verify it mirrors the README ({e}) - verify by hand"))
if dh is not None and dh.strip() != want:
findings.append(("LETTER", f"description: the Docker Hub short description ('{dh.strip()}') does not match the README intro ('{want}') - set it from the README (spec/readme-structure.md)"))

# --- cspell single source of truth ---
# CODESTYLE.md "Markdown and Spelling": cspell.json is the one word list, and a cSpell words block left in
Expand Down
4 changes: 2 additions & 2 deletions spec/readme-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The preferred `README.md` shape for a fleet project. The audit's `readme-structu

## Sections and Order

1. **Title (`# <Name>`)** - the repo name, then a one-line description as the next paragraph. That line is **link-free plain text**: it doubles as the GitHub About description (AGENTS.md "Repository Details"), which renders no markdown, so a link would carry as raw brackets. The audit checks both properties.
1. **Title (`# <Name>`)** - the H1 **is the repository name** (a hyphenated name may render its hyphens as spaces: `Financial-Modeling` -> `Financial Modeling`), then a one-line description as the next paragraph. That description is a **single sentence, link-free plain text, at most 100 characters** - it is the one canonical short description. It doubles as the GitHub About description (AGENTS.md "Repository Details") and, for a repo that publishes a Docker image, the Docker Hub short description. Both render no markdown, and Docker Hub caps the short description near 100 characters - the tightest surface, which sets the limit. The audit checks the H1 name, the length, the link-free form, and the mirrors.
2. **Build and Distribution (`##`)** - a bullet per distribution channel the project actually ships, each linking where it lives: **Source Code** (the GitHub repo), **Versioned Releases** (GitHub Releases), **Docker Images** (Docker Hub), **NuGet Packages** (NuGet.org), **PyPI Packages** (PyPI.org). List only the channels the project uses. It carries three sub-sections:
- **Build Status (`###`)** - the CI/build status shields (release build, Docker build, last commit, last build).
- **Releases (`###`)** - the version shields (GitHub release, GitHub pre-release, Docker latest/develop, NuGet, PyPI), one per channel the project publishes.
Expand Down Expand Up @@ -40,4 +40,4 @@ Shields are not a top-level section - they live under **Build and Distribution**

## Docker Hub README

A repo that publishes a Docker image keeps a **separate** `Docker/README.md` for the Docker Hub repository overview: Docker Hub's description has a much smaller size limit than a project README, so it carries a trimmed overview, not the full README. It is published by the docker-readme workflow task, not copied from the root README.
Docker Hub has two text fields: a **short description** (the tagline, capped near 100 characters) that mirrors the README intro line (item 1), and the longer **overview**. A repo that publishes a Docker image keeps a **separate** `Docker/README.md` for the overview: Docker Hub's description has a much smaller size limit than a project README, so it carries a trimmed overview, not the full README. It is published by the docker-readme workflow task, not copied from the root README.