Skip to content

Package checks

noren checks the contents of selected files from the chosen Git commit. It does not inspect package archives, their member names, or files generated by a build backend. An sdist's PKG-INFO, a wheel's METADATA, and a generated version file need a separate check.

Keep private settings out of packages

The committed word list contains identifiers you intend to keep private. Excluding it in noren.yaml controls the Git snapshot only; it does not control an sdist or wheel built from the source repository.

Prefer a checkout of the destination tag as the build input. If you must build from the source repository, configure that build backend's file selection too. For example, with Hatchling:

[tool.hatch.build.targets.sdist]
exclude = ["/noren.yaml", "/noren-words.txt"]

This example excludes two files; it does not establish that every other file is public. Reported Hatchling configurations included development instructions and directories in sdists before their selection was tightened. That observation does not establish default behavior for all backends.

A project may instead use an explicit selection, for example:

[tool.hatch.build.targets.sdist]
only-include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]

Choose paths for your package, including any needed data. If only-include already omits the settings, another exclusion for those settings does not prove the rest of the archive is correct. Build hooks, artifacts, and forced inclusions can affect the final contents. Review their configuration and the produced files. See Hatchling's file-selection documentation.

Inspect what was built

Build into an empty output directory so an old artifact cannot be uploaded accidentally. Check both sdist and wheel, even when their names and versions look correct.

  1. List all archive member names. Check for development files, settings, unexpected data, and private names in paths.
  2. Scan every member's name and contents with the excluded word list, using NFKC normalization and case folding. Include generated metadata and version files.
  3. Check package metadata, the intended version, license and bundled-data licenses, dependencies, and public project and documentation URLs.
  4. Inspect public pyproject.toml and lock files for private Git URLs, local paths, and private package sources. These can survive even if the code itself contains no private names.
  5. Validate the distribution metadata and rendered README. Use absolute public links or anchors within the same page when a registry cannot resolve repository-relative links and images.

The following is an example scan of Python sdists and wheels in a fresh dist/. Run it in the source repository where the excluded word list is available, with only the reviewed artifacts in dist/. It reads archives without extracting them and reports member paths; keep that output private because paths may contain private names. It is a substring check, not a general secret scanner or license audit.

python - <<'PY'
from pathlib import Path
import tarfile
import unicodedata
import zipfile

def normalized(text):
    return unicodedata.normalize("NFKC", text).casefold()

words = [
    normalized(line.strip())
    for line in Path("noren-words.txt").read_text(encoding="utf-8").splitlines()
    if line.strip()
]
artifacts = sorted(Path("dist").glob("*.tar.gz")) + sorted(Path("dist").glob("*.whl"))
if not words or not artifacts:
    raise SystemExit("A nonempty word list and built artifacts are required")

failures = []

def inspect(artifact, name, data):
    text = normalized(name + "\n" + data.decode("utf-8", errors="replace"))
    if any(word in text for word in words):
        failures.append(f"{artifact.name}: {name}")

for artifact in artifacts:
    if artifact.suffix == ".whl":
        with zipfile.ZipFile(artifact) as archive:
            for member in archive.infolist():
                inspect(artifact, member.filename, archive.read(member))
    else:
        with tarfile.open(artifact) as archive:
            for member in archive:
                stream = archive.extractfile(member) if member.isfile() else None
                data = stream.read() if stream else member.linkname.encode()
                inspect(artifact, member.name, data)

if failures:
    raise SystemExit("Forbidden content in:\n" + "\n".join(failures))
print(f"Checked {len(artifacts)} artifacts")
PY

Run additional project checks before upload. For example:

uvx twine check dist/*.tar.gz dist/*.whl

Do not replace archive inspection with a test that merely compares configuration entries. In one report, an exclusion test added no protection because an existing allow list already left those files out; inspecting the real sdist exposed what the configuration test did not establish.

Treat registry publication as persistent

Complete noren's check and inspect packages before upload. In one reported case, a deleted registry file remained downloadable from a CDN URL about 45 hours later. This establishes neither a retention limit nor a deletion guarantee for that registry or any other. Correct the issue before the first upload rather than depending on deletion afterward.

Verify the download and metadata

After upload, compare the registry's version, license, project URLs, and file hashes against the artifacts you inspected. For example, for a PyPI project and version you own:

curl --fail https://pypi.org/pypi/PROJECT/1.2.3/json
sha256sum dist/project-1.2.3.tar.gz dist/project-1.2.3-py3-none-any.whl

Replace the project, version, and filenames. PyPI's JSON API documentation describes the metadata and distribution digests. Check that both the published page and the downloadable files are the intended ones.

A report observed a version-specific JSON endpoint returning 404 while the project endpoint already listed that version just after upload. Caching was suspected, but the cause was not isolated or reproduced. Compare available metadata and downloads; one failed endpoint does not establish that publication failed, and repeated upload is not a substitute for verification.