Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
41 changes: 35 additions & 6 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import re
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
from typing import Any

Expand Down Expand Up @@ -74,6 +75,22 @@ 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 unavailable.

The image name is taken as owner/repo lowercased, the fleet convention (`ptr727/PhotoCleaner` ->
`ptr727/photocleaner`). Best-effort: a repo whose Docker Hub image is named otherwise, or not yet pushed,
404s and is skipped rather than falsely flagged. Read-only, unauthenticated, so it needs no token.
"""
owner, repo = slug.split("/", 1)
url = f"https://hub.docker.com/v2/repositories/{owner.lower()}/{repo.lower()}/"
try:
with urllib.request.urlopen(url, timeout=15) as r:
return json.loads(r.read().decode("utf-8")).get("description")
Comment thread
ptr727 marked this conversation as resolved.
Outdated
except Exception:
return None


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 +709,33 @@ 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; a hyphenated name may render its hyphens as spaces.
repo_name = slug.split("/")[-1]
if 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)"))
Comment thread
ptr727 marked this conversation as resolved.
Outdated
Comment thread
ptr727 marked this conversation as resolved.
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.
if any((pt.get("target") if isinstance(pt, dict) else pt) == "docker" for pt in entry.get("publish", [])):
dh = docker_hub_description(slug)
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)"))
Comment thread
ptr727 marked this conversation as resolved.

# --- 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.