From 67a6ec4ebff6bb191a066f889463335c864b070e Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 3 Oct 2026 12:01:56 +1000 Subject: [PATCH 1/2] Fix audit asset checks and version-manifest validation Report a missing local non-Markdown asset from audit as DK004, matching check's build failure instead of a false Ready to publish. Report a missing Git binary from check-release and build-all as an actionable DocSproutError instead of an unhandled traceback. Reject versions.json release names reserved for generated output and names that differ only by case, which crashed build-all on case-insensitive filesystems. --- src/docsprout/audit.py | 19 +++++++++++++---- src/docsprout/versions.py | 21 ++++++++++++++++-- tests/test_audit.py | 23 ++++++++++++++++++++ tests/test_versions.py | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/docsprout/audit.py b/src/docsprout/audit.py index 065dd7a..5a1da36 100644 --- a/src/docsprout/audit.py +++ b/src/docsprout/audit.py @@ -86,6 +86,17 @@ def _is_safe_local_file(root: Path, reference: str) -> bool: return candidate.is_relative_to(root) and candidate.is_file() +def _missing_local_asset(page: _AuditPage, line: int, target: str, root: Path) -> Finding | None: + """Report a local reference that cannot be published as a content asset.""" + requested_source = _source_target(page, target.partition("#")[0]) + if _is_safe_document_reference(requested_source) and _is_safe_local_file(root, requested_source): + return None + return _finding( + "DK004", "error", page, line, "Missing local image or asset", + "Add the asset under the documentation content path or correct the reference.", target, + ) + + def _audit_link(page: _AuditPage, line: int, target: str, pages: dict[str, _AuditPage], root: Path) -> Finding | None: parsed = urlsplit(target) if _is_external(target): @@ -98,7 +109,7 @@ def _audit_link(page: _AuditPage, line: int, target: str, pages: dict[str, _Audi elif document.lower().endswith(".md"): requested_source = _source_target(page, document) else: - return None + return _missing_local_asset(page, line, target, root) if not _is_safe_document_reference(requested_source): return _finding("DK005", "error", page, line, "Local link escapes documentation paths", "Use a published docs page or the repository README.", target) target_page = pages.get(requested_source) @@ -124,9 +135,9 @@ def _audit_image(page: _AuditPage, line: int, alt: str, target: str, root: Path) asset = target.partition("#")[0] if not asset: return findings - requested_source = _source_target(page, asset) - if not _is_safe_document_reference(requested_source) or not _is_safe_local_file(root, requested_source): - findings.append(_finding("DK004", "error", page, line, "Missing local image or asset", "Add the asset under the documentation content path or correct the reference.", target)) + finding = _missing_local_asset(page, line, target, root) + if finding is not None: + findings.append(finding) return findings diff --git a/src/docsprout/versions.py b/src/docsprout/versions.py index 2db05f7..30ef2cb 100644 --- a/src/docsprout/versions.py +++ b/src/docsprout/versions.py @@ -20,6 +20,7 @@ MOVING_REFS = {"head", "main", "master", "develop", "development", "latest"} COMMIT = re.compile(r"^[0-9a-fA-F]{40}$") RELEASE_NAME = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]*$") +RESERVED_RELEASE_NAMES = {"index.html", "versions.json"} def _valid_source_ref(value: str) -> bool: @@ -78,6 +79,10 @@ def load_manifest(root: Path) -> VersionManifest: raise DocSproutError( f"{path}: versions[{index}].release must be a safe name using letters, numbers, dots, underscores, or hyphens" ) + if release.casefold() in RESERVED_RELEASE_NAMES: + raise DocSproutError( + f"{path}: versions[{index}].release {release!r} is reserved for generated site output; choose another release name" + ) if not _valid_source_ref(source_ref): raise DocSproutError(f"{path}: versions[{index}].source_ref must be a safe tag or full commit SHA") versions.append(Version(release, source_ref)) @@ -85,11 +90,23 @@ def load_manifest(root: Path) -> VersionManifest: source_refs = [entry.source_ref for entry in versions] if len(set(releases)) != len(releases) or len(set(source_refs)) != len(source_refs) or current not in releases: raise DocSproutError(f"{path}: releases and source refs must be unique, and releases must include current {current!r}") + folded_releases = [release.casefold() for release in releases] + if len(set(folded_releases)) != len(folded_releases): + raise DocSproutError( + f"{path}: releases must be unique even on case-insensitive filesystems; rename one of the duplicated releases" + ) return VersionManifest(current, tuple(versions)) +def _git_completed(root: Path, *arguments: str, binary: bool = False) -> subprocess.CompletedProcess: + try: + return subprocess.run(["git", *arguments], cwd=root, capture_output=True, text=not binary) + except OSError as error: + raise DocSproutError("Git is required for build-all and check-release; install Git and try again.") from error + + def _run_git(root: Path, *arguments: str, binary: bool = False) -> str | bytes: - completed = subprocess.run(["git", *arguments], cwd=root, capture_output=True, text=not binary) + completed = _git_completed(root, *arguments, binary=binary) if completed.returncode: detail = completed.stderr.decode() if binary else completed.stderr raise DocSproutError(f"Git {' '.join(arguments)} failed: {detail.strip()}") @@ -101,7 +118,7 @@ def _is_immutable(root: Path, source_ref: str) -> bool: return False if COMMIT.fullmatch(source_ref): return True - completed = subprocess.run(["git", "show-ref", "--verify", "--quiet", f"refs/tags/{source_ref}"], cwd=root) + completed = _git_completed(root, "show-ref", "--verify", "--quiet", f"refs/tags/{source_ref}") return completed.returncode == 0 diff --git a/tests/test_audit.py b/tests/test_audit.py index f699f4e..f817dc9 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -238,6 +238,29 @@ def test_audit_still_checks_genuine_links_and_images(self) -> None: self.assertEqual(["DK001", "DK004"], [finding.code for finding in findings]) + def test_audit_reports_missing_local_assets_linked_from_prose(self) -> None: + from docsprout.audit import audit_project + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _write_project(root, {"index.md": "# Home\n\n[Download](files/manual.zip)\n"}) + + findings = audit_project(root).findings + + self.assertEqual(["DK004"], [finding.code for finding in findings]) + self.assertEqual("docs/index.md", findings[0].file) + self.assertEqual(3, findings[0].line) + self.assertEqual("files/manual.zip", findings[0].target) + + def test_audit_accepts_existing_linked_local_assets(self) -> None: + from docsprout.audit import audit_project + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _write_project(root, {"index.md": "# Home\n\n[Download](files/manual.zip)\n", "files/manual.zip": "archive"}) + + self.assertEqual((), audit_project(root).findings) + def test_audit_reports_unsafe_url_schemes_instead_of_treating_them_as_external(self) -> None: from docsprout.audit import audit_project diff --git a/tests/test_versions.py b/tests/test_versions.py index 2857047..3b5835c 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -267,6 +267,51 @@ def test_check_release_explains_how_to_create_a_missing_tag(self) -> None: with self.assertRaisesRegex(DocSproutError, r"does not exist.*Create the tag"): check_release(root) + def test_check_release_reports_a_missing_git_binary_as_a_docsprout_error(self) -> None: + from unittest.mock import patch + + root = self._repository() + + with patch("docsprout.versions.subprocess.run", side_effect=FileNotFoundError("git")): + with self.assertRaisesRegex(DocSproutError, "Git is required"): + check_release(root) + + def test_run_git_reports_a_missing_git_binary_as_a_docsprout_error(self) -> None: + from unittest.mock import patch + + import docsprout.versions as versions_module + + with tempfile.TemporaryDirectory() as temporary: + with patch("docsprout.versions.subprocess.run", side_effect=FileNotFoundError("git")): + with self.assertRaisesRegex(DocSproutError, "Git is required"): + versions_module._run_git(Path(temporary), "rev-parse", "HEAD") + + def test_manifest_rejects_release_names_reserved_for_generated_output(self) -> None: + root = self._repository() + manifest = root / "docs" / "versions.json" + + for reserved in ("versions.json", "Index.html"): + with self.subTest(reserved=reserved): + data = json.loads(manifest.read_text(encoding="utf-8")) + data["current"] = reserved + data["versions"][0]["release"] = reserved + manifest.write_text(json.dumps(data), encoding="utf-8") + + with self.assertRaisesRegex(DocSproutError, r"reserved"): + load_manifest(root) + + def test_manifest_rejects_case_insensitive_duplicate_releases(self) -> None: + root = self._repository() + manifest = root / "docs" / "versions.json" + data = json.loads(manifest.read_text(encoding="utf-8")) + data["versions"][0]["release"] = "Stable" + data["versions"][1]["release"] = "stable" + data["current"] = "stable" + manifest.write_text(json.dumps(data), encoding="utf-8") + + with self.assertRaisesRegex(DocSproutError, r"case-insensitive"): + load_manifest(root) + def test_check_release_rejects_uncommitted_documentation(self) -> None: root = self._repository() (root / "docs" / "new.md").write_text("# Changed after tagging", encoding="utf-8") From c1e16bb82f24ebfbd0a9d90641f3bca8c72c44ab Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 3 Oct 2026 12:02:09 +1000 Subject: [PATCH 2/2] Prepare v1.3.1 documentation Diagnostics and release-validation patch: refresh the package version, release manifest, workflow pins, guides, qualification evidence, migration notes and changelog together. No configuration, route, machine-format, public CSS token or dependency change. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/publish-docs.yml | 2 +- CHANGELOG.md | 18 +++++++++++++++ README.md | 4 ++-- docs/architecture.md | 2 +- docs/beginners-guide.md | 4 ++-- docs/building.md | 2 +- docs/ci-cd.md | 6 ++--- docs/customisation.md | 4 ++-- docs/existing-repository.md | 2 +- docs/github-pages.md | 10 ++++----- docs/historical-docs.md | 2 +- docs/homepage-recipes.md | 2 +- docs/migration.md | 22 ++++++++++++++----- docs/pre-publish-checklist.md | 14 ++++++------ docs/qualification.md | 16 +++++++++++--- docs/themes.md | 4 ++-- docs/versions.json | 3 ++- .../.github/workflows/docsprout-pages.yml | 2 +- .../.github/workflows/documentation.yml | 2 +- .../.github/workflows/documentation.yml | 2 +- pyproject.toml | 2 +- src/docsprout/__init__.py | 2 +- tests/qualification_installed.py | 2 +- tests/test_documentation.py | 8 +++---- tests/test_workflows.py | 6 ++--- 26 files changed, 93 insertions(+), 52 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 93d1ad3..d18dc23 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: attributes: label: DocSprout version description: Output of `docsprout --version`. - placeholder: docsprout 1.3.0 + placeholder: docsprout 1.3.1 validations: required: true - type: input diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 533a386..13d4475 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/setup-python@v7 with: python-version: '3.11' - - run: python -m pip install "git+https://github.com/ikelaiah/docsprout.git@v1.3.0" + - run: python -m pip install "git+https://github.com/ikelaiah/docsprout.git@v1.3.1" - run: docsprout check - if: inputs.versioned run: docsprout check-release diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1068b..6793992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.3.1 + +A diagnostics and release-validation patch. No configuration, route, +machine-format, public CSS token or dependency change is required. Update +package and workflow pins to `v1.3.1`, then rebuild. + +### Fixed + +- `audit` now reports a missing local non-Markdown asset, such as a linked ZIP + or PDF, as DK004, matching the documented rule and `check`'s build + behaviour. Previously `audit` could report "Ready to publish" while `build` + failed. +- `check-release` and `build-all` report an actionable "Git is required" error + when Git is not installed, instead of an unhandled traceback. +- `versions.json` rejects release names reserved for generated site output + (`versions.json`, `index.html`) and release names that differ only by case, + which previously crashed `build-all` on case-insensitive filesystems. + ## 1.3.0 A visual-theme modernisation minor. No configuration, route, machine-format, diff --git a/README.md b/README.md index ed43feb..320cc1c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🌱 DocSprout -[![CI](https://github.com/ikelaiah/docsprout/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ikelaiah/docsprout/actions/workflows/ci.yml?query=branch%3Amain) [![Docs](https://img.shields.io/website?url=https%3A%2F%2Fikelaiah.github.io%2Fdocsprout%2F1.3.0%2F&label=docs)](https://ikelaiah.github.io/docsprout/1.3.0/) [![Latest release](https://img.shields.io/github/v/release/ikelaiah/docsprout?display_name=tag&sort=semver)](https://github.com/ikelaiah/docsprout/releases/latest) [![Python 3.10–3.14](https://img.shields.io/badge/python-3.10%E2%80%933.14-3776AB?logo=python&logoColor=white)](https://github.com/ikelaiah/docsprout/blob/main/docs/qualification.md#supported-python-versions) [![License: MIT](https://img.shields.io/github/license/ikelaiah/docsprout)](https://github.com/ikelaiah/docsprout/blob/main/LICENSE) +[![CI](https://github.com/ikelaiah/docsprout/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ikelaiah/docsprout/actions/workflows/ci.yml?query=branch%3Amain) [![Docs](https://img.shields.io/website?url=https%3A%2F%2Fikelaiah.github.io%2Fdocsprout%2F1.3.1%2F&label=docs)](https://ikelaiah.github.io/docsprout/1.3.1/) [![Latest release](https://img.shields.io/github/v/release/ikelaiah/docsprout?display_name=tag&sort=semver)](https://github.com/ikelaiah/docsprout/releases/latest) [![Python 3.10–3.14](https://img.shields.io/badge/python-3.10%E2%80%933.14-3776AB?logo=python&logoColor=white)](https://github.com/ikelaiah/docsprout/blob/main/docs/qualification.md#supported-python-versions) [![License: MIT](https://img.shields.io/github/license/ikelaiah/docsprout)](https://github.com/ikelaiah/docsprout/blob/main/LICENSE) ![DocSprout banner — Build documentation people can use.](docs/assets/docsprout-banner.svg) @@ -23,7 +23,7 @@ Open a terminal in your project's top-level folder—the folder that normally contains files such as `README.md`, `src/` or `.git/`—and run: ```bash -python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.0.zip" +python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.1.zip" docsprout init docsprout serve ``` diff --git a/docs/architecture.md b/docs/architecture.md index c5e1382..5dcd90c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -77,6 +77,6 @@ needs no custom CSS; the custom stylesheet mechanism is a deliberately bounded escape hatch whose inclusion mechanics DocSprout owns and whose accessibility the author owns. -Downstream projects should pin released tags such as `v1.3.0`, never the main +Downstream projects should pin released tags such as `v1.3.1`, never the main branch. The stable boundary and deprecation policy are in [Machine-readable contracts](machine-contracts.md). diff --git a/docs/beginners-guide.md b/docs/beginners-guide.md index 5cf11d6..aa7f487 100644 --- a/docs/beginners-guide.md +++ b/docs/beginners-guide.md @@ -26,7 +26,7 @@ that normally contains `README.md` or `src/`. Open a terminal in your project folder and run: ```bash -python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.0.zip" +python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.1.zip" ``` Then check that the command is available: @@ -254,7 +254,7 @@ appear in the navigation. in [How DocSprout renders Markdown](markdown-showcase.md). - Learn the three configuration files in [Configuration](configuration.md). - Copy a small working project from the - [minimal example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/minimal). + [minimal example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/minimal). - When you truly want a public site, choose the simpler or historical path in [GitHub Pages](github-pages.md). - Look up unfamiliar words in the [glossary](glossary.md). diff --git a/docs/building.md b/docs/building.md index ae21ea1..701f107 100644 --- a/docs/building.md +++ b/docs/building.md @@ -24,7 +24,7 @@ asset is local, and `index.html` opens directly in a browser. ## Ship a ZIP ```bash -docsprout build --release 1.3.0 --offline-archive dist/docsprout-docs-1.3.0.zip +docsprout build --release 1.3.1 --offline-archive dist/docsprout-docs-1.3.1.zip ``` The release label used for an offline archive must start with a letter or diff --git a/docs/ci-cd.md b/docs/ci-cd.md index d0c943a..66ff3c5 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -40,7 +40,7 @@ jobs: with: python-version: "3.12" - name: Install DocSprout - run: python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.0.zip" + run: python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.1.zip" - name: Validate the site run: docsprout check - name: Audit publications @@ -52,7 +52,7 @@ jobs: path: audit.json ``` -Pin a released tag (`v1.3.0` above), never a branch. `--format json` writes the +Pin a released tag (`v1.3.1` above), never a branch. `--format json` writes the machine-readable report; the artifact keeps it after the job, including when the audit fails. @@ -76,7 +76,7 @@ is yours. The same checks in GitLab CI: docs-checks: image: python:3.12 script: - - python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.0.zip" + - python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.1.zip" - docsprout check - docsprout audit --strict ``` diff --git a/docs/customisation.md b/docs/customisation.md index e7ab86a..c678a42 100644 --- a/docs/customisation.md +++ b/docs/customisation.md @@ -70,8 +70,8 @@ identity asset: Fields that are not relevant here, such as `banner` and `homepage`, are omitted for brevity. You can inspect the complete maintained -[`docs/docsprout.json`](https://github.com/ikelaiah/docsprout/blob/v1.3.0/docs/docsprout.json) -and its [local SVG asset](https://github.com/ikelaiah/docsprout/blob/v1.3.0/docs/assets/docsprout-mark.svg). +[`docs/docsprout.json`](https://github.com/ikelaiah/docsprout/blob/v1.3.1/docs/docsprout.json) +and its [local SVG asset](https://github.com/ikelaiah/docsprout/blob/v1.3.1/docs/assets/docsprout-mark.svg). On generated DocSprout pages, purple is the accent and highlight colour; the potted sprout mark (green leaves, brown soil and a beige pot) appears immediately before the project name in the top header; diff --git a/docs/existing-repository.md b/docs/existing-repository.md index 1ef6530..bb4674b 100644 --- a/docs/existing-repository.md +++ b/docs/existing-repository.md @@ -18,7 +18,7 @@ Run both commands from the repository's top-level folder—the one that contains `README.md` or `.git/`: ```bash -python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.0.zip" +python -m pip install "https://github.com/ikelaiah/docsprout/archive/refs/tags/v1.3.1.zip" docsprout init ``` diff --git a/docs/github-pages.md b/docs/github-pages.md index 64d3d3e..8535751 100644 --- a/docs/github-pages.md +++ b/docs/github-pages.md @@ -48,7 +48,7 @@ from feature branches do not deploy Pages. workflow are current, it makes no repository changes. The generated workflow carries a DocSprout ownership marker and pins the reusable -workflow to the installed release, such as `@v1.3.0`. It never follows +workflow to the installed release, such as `@v1.3.1`. It never follows `@main`. If a recognised managed workflow is older, ordinary setup reports the version and leaves it unchanged. Update only that workflow deliberately: @@ -101,13 +101,13 @@ jobs: contents: read pages: write id-token: write - uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.0 + uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.1 with: versioned: false release: latest ``` -The maintained [single-version example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/single-version) +The maintained [single-version example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/single-version) uses this form. ## Historical site for versioned projects @@ -129,7 +129,7 @@ jobs: contents: read pages: write id-token: write - uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.0 + uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.1 ``` Check a historical release locally after creating its tag and before pushing: @@ -140,7 +140,7 @@ docsprout check-release docsprout build-all --output build/docs-site ``` -The maintained [historical example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/historical) +The maintained [historical example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/historical) uses this form. Follow the [pre-publish checklist](pre-publish-checklist.md) for the exact release order. diff --git a/docs/historical-docs.md b/docs/historical-docs.md index cb28c05..540269b 100644 --- a/docs/historical-docs.md +++ b/docs/historical-docs.md @@ -71,6 +71,6 @@ the release check and full build, then push the tag. Use the exact commands in t [pre-publish checklist](pre-publish-checklist.md). The maintained -[historical example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/historical) +[historical example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/historical) demonstrates two small releases. Its automated test builds the same Git history twice and compares every generated byte. diff --git a/docs/homepage-recipes.md b/docs/homepage-recipes.md index e85b41d..db848c9 100644 --- a/docs/homepage-recipes.md +++ b/docs/homepage-recipes.md @@ -18,7 +18,7 @@ which is a top-level setting, are `homepage` values. ## See it in DocSprout DocSprout uses the same configuration on this site. Its -[`docs/docsprout.json`](https://github.com/ikelaiah/docsprout/blob/v1.3.0/docs/docsprout.json) +[`docs/docsprout.json`](https://github.com/ikelaiah/docsprout/blob/v1.3.1/docs/docsprout.json) uses these exact values for its home-page banner and capability cards: ```json diff --git a/docs/migration.md b/docs/migration.md index 1f46ab8..b744277 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -3,7 +3,7 @@ The 1.x upgrades are deliberately boring: v1.0.0 froze the qualified v0.18.1 surface, and every later 1.x release adds no configuration schema change. Pin the released package and workflow, then run the same checks you already use. -Current guidance targets `v1.3.0`; each section below remains authoritative for +Current guidance targets `v1.3.1`; each section below remains authoritative for its own step. ## DocKit to DocSprout (1.x rebrand) @@ -64,7 +64,7 @@ each published release manifest on immutable tags while upgrading. ## Upgrade directly from any supported 0.x release All released 0.x configurations use schema version 1. Upgrade the pinned package -and workflow to the latest 1.x release (`v1.3.0`), then use this table before +and workflow to the latest 1.x release (`v1.3.1`), then use this table before running `doctor`, `check`, and the appropriate publish build. | Starting release | Required compatibility work | @@ -96,6 +96,18 @@ segments and option-like refs, requires the current source to match `HEAD`, and requires documentation changes to be committed. These checks make existing valid manifests more dependable; they do not change generated routes. +## v1.3.0 to v1.3.1 + +No configuration or content migration is required. Update package and workflow +pins to `v1.3.1`, then rebuild. The patch tightens diagnostics and release +validation without changing generated routes, configuration schemas, the +audit JSON format or public CSS tokens: `audit` now reports a missing local +non-Markdown asset as DK004 just as `check` fails the build, `check-release` +and `build-all` report an actionable "Git is required" error instead of an +unhandled traceback when Git is unavailable, and `versions.json` rejects +release names reserved for generated output and names that differ only by +case. + ## v1.2.3 to v1.3.0 No configuration or content migration is required. Update package and workflow @@ -305,7 +317,7 @@ the contract qualified by v0.18.1: 1. Pin the package archive or source installation to `v1.0.0` and pin the reusable Pages workflow to `@v1.0.0`; never use `main`. For a new upgrade - today, pin the latest 1.x release (`v1.3.0`) instead. + today, pin the latest 1.x release (`v1.3.1`) instead. 2. Keep `"schema_version": 1` in `docsprout.json`, `layout.json` and `versions.json`. There is no schema rewrite or generated-route migration. 3. If custom CSS still uses pre-v0.18 generic names such as `--bg`, `--text` @@ -437,9 +449,9 @@ safe, readable plain code. Version 1.0 is the formal commitment point for the contracts that v0.18.1 qualified, and later 1.x releases keep that contract. There is no schema change. -Before adopting the latest 1.x release (`v1.3.0`): +Before adopting the latest 1.x release (`v1.3.1`): -1. Upgrade the pinned package and Pages workflow to `v1.3.0` following this +1. Upgrade the pinned package and Pages workflow to `v1.3.1` following this guide's patterns (config-compatible, then rebuild). 2. Remove references to any pre-v0.18 generic theme token names (`--bg`, `--text`, `--interactive`, …) in custom CSS; the `--dk-*` names diff --git a/docs/pre-publish-checklist.md b/docs/pre-publish-checklist.md index 15d4507..1c01fc9 100644 --- a/docs/pre-publish-checklist.md +++ b/docs/pre-publish-checklist.md @@ -62,10 +62,10 @@ This path updates the public site from a branch such as `main`. ## Historical site This path publishes an immutable site for every listed release. In the example -below, replace `1.3.0` with your version. +below, replace `1.3.1` with your version. -1. Add release `1.3.0` and source ref `v1.3.0` to `docs/versions.json`. Set - `current` to `1.3.0`. +1. Add release `1.3.1` and source ref `v1.3.1` to `docs/versions.json`. Set + `current` to `1.3.1`. 2. Run `docsprout check`. 3. Commit all release files, including package metadata, documentation, examples and workflows, on a branch named `release/vX.Y.Z`: @@ -73,7 +73,7 @@ below, replace `1.3.0` with your version. ```bash git add pyproject.toml src/docsprout/__init__.py README.md CHANGELOG.md docs .github examples tests git diff --cached --check - git commit -m "Prepare v1.3.0 documentation" + git commit -m "Prepare v1.3.1 documentation" ``` 4. Push the qualified branch and open a PR against `main`. Wait for every @@ -83,7 +83,7 @@ below, replace `1.3.0` with your version. 5. Check out the merged `main` commit and create the tag on that exact commit: ```bash - git tag -a v1.3.0 -m "v1.3.0" + git tag -a v1.3.1 -m "v1.3.1" ``` 6. Run the release checks and historical build: @@ -100,7 +100,7 @@ below, replace `1.3.0` with your version. 8. Push the annotated tag (the merge commit is already on `main`): ```bash - git push origin v1.3.0 + git push origin v1.3.1 ``` 9. Create the GitHub Release from the tag. Confirm both CI and the Documentation @@ -111,7 +111,7 @@ below, replace `1.3.0` with your version. ## If you find a mistake If the tag exists only on your computer, correct and commit the files, delete -the local tag with `git tag -d v1.3.0`, then create it again on the corrected +the local tag with `git tag -d v1.3.1`, then create it again on the corrected commit. If the tag has been pushed, do not move or replace it. Publish the correction diff --git a/docs/qualification.md b/docs/qualification.md index 49d8fc2..546a54f 100644 --- a/docs/qualification.md +++ b/docs/qualification.md @@ -1,7 +1,7 @@ -# Qualification evidence for DocSprout v1.3.0 +# Qualification evidence for DocSprout v1.3.1 -DocSprout v1.3.0 modernises every visual style while preserving JSON and -Markdown layout compatibility and the v1.0.0 stable contract. The claims below are the contract that CI and the maintained fixtures +DocSprout v1.3.1 is a diagnostics and release-validation patch that preserves +JSON and Markdown layout compatibility and the v1.0.0 stable contract. The claims below are the contract that CI and the maintained fixtures exercise. Every row names how it is verified. "Supported" means the combination is run by automated qualification on every pull request and release, not merely believed to work. @@ -286,6 +286,16 @@ site configuration; no schema, route or token value changes: distinguish page structure from home-page presentation, and the glossary covers the navigation, theme and command vocabulary. +## v1.3.1 additions + +- Audit tests cover a missing local non-Markdown asset linked from prose + (DK004) and an existing asset that stays quiet, matching the documented + DK004 rule and `check`'s build behaviour. +- Version tests cover an actionable "Git is required" error for `check-release` + and `_run_git` when Git is unavailable, plus manifest rejection of release + names reserved for generated output (`versions.json`, `index.html`) and + names that differ only by case on case-insensitive filesystems. + ## v1.3.0 additions - Generated-page tests cover the glassmorphic hero glow, specular panel diff --git a/docs/themes.md b/docs/themes.md index d05e22b..7a02c68 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -46,7 +46,7 @@ footer—never underlines, signalling hover with a background tint or colour shift instead, and buttons never underline. The maintained -[single-version example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/single-version) +[single-version example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/single-version) starts in Paper (`"style": "paper"`), so its built opening page shows the warm reading surface before a visitor changes the control. @@ -119,7 +119,7 @@ hexadecimal values: Test custom colours in both Light and Dark mode. Links, selected navigation and keyboard focus must remain easy to see. A preset is safer when you are unsure. The maintained -[minimal example](https://github.com/ikelaiah/docsprout/tree/v1.3.0/examples/minimal) +[minimal example](https://github.com/ikelaiah/docsprout/tree/v1.3.1/examples/minimal) uses the exact teal values shown above; its built links, selected navigation and focus state use that accent. diff --git a/docs/versions.json b/docs/versions.json index 60ec044..3238463 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,7 +1,8 @@ { "schema_version": 1, - "current": "1.3.0", + "current": "1.3.1", "versions": [ + {"release": "1.3.1", "source_ref": "v1.3.1"}, {"release": "1.3.0", "source_ref": "v1.3.0"}, {"release": "1.2.3", "source_ref": "v1.2.3"}, {"release": "1.2.2", "source_ref": "v1.2.2"}, diff --git a/examples/github-pages/.github/workflows/docsprout-pages.yml b/examples/github-pages/.github/workflows/docsprout-pages.yml index 40edef6..6ff13a2 100644 --- a/examples/github-pages/.github/workflows/docsprout-pages.yml +++ b/examples/github-pages/.github/workflows/docsprout-pages.yml @@ -13,7 +13,7 @@ concurrency: jobs: documentation: if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) - uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.0 + uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.1 with: versioned: false release: latest diff --git a/examples/historical/.github/workflows/documentation.yml b/examples/historical/.github/workflows/documentation.yml index 8b475dc..57188fa 100644 --- a/examples/historical/.github/workflows/documentation.yml +++ b/examples/historical/.github/workflows/documentation.yml @@ -12,7 +12,7 @@ permissions: jobs: documentation: - uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.0 + uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.1 permissions: contents: read pages: write diff --git a/examples/single-version/.github/workflows/documentation.yml b/examples/single-version/.github/workflows/documentation.yml index 8c02f1f..0f6e8ee 100644 --- a/examples/single-version/.github/workflows/documentation.yml +++ b/examples/single-version/.github/workflows/documentation.yml @@ -12,7 +12,7 @@ permissions: jobs: documentation: - uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.0 + uses: ikelaiah/docsprout/.github/workflows/publish-docs.yml@v1.3.1 with: versioned: false release: latest diff --git a/pyproject.toml b/pyproject.toml index 45e3d3c..3aec21f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "docsprout" -version = "1.3.0" +version = "1.3.1" description = "Versioned static documentation sites for code projects" keywords = ["documentation", "static-site", "markdown", "github-pages"] readme = "README.md" diff --git a/src/docsprout/__init__.py b/src/docsprout/__init__.py index 991f915..c0e984e 100644 --- a/src/docsprout/__init__.py +++ b/src/docsprout/__init__.py @@ -1,3 +1,3 @@ """DocSprout: versioned static documentation sites for code projects.""" -__version__ = "1.3.0" +__version__ = "1.3.1" diff --git a/tests/qualification_installed.py b/tests/qualification_installed.py index 28348c4..25fc444 100644 --- a/tests/qualification_installed.py +++ b/tests/qualification_installed.py @@ -5,7 +5,7 @@ source tree. Every path used here is a fresh temporary directory, so a passing run is evidence that the package does not depend on repository-only files. - python qualification_installed.py --expected-version 1.3.0 + python qualification_installed.py --expected-version 1.3.1 The script is deliberately self-contained (stdlib only) so it can be copied or run from any location without importing anything from the repository. diff --git a/tests/test_documentation.py b/tests/test_documentation.py index c802817..6b7017e 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -93,13 +93,13 @@ def test_recommended_layouts_use_the_explicit_modern_contract(self) -> None: def test_release_metadata_and_version_manifest_agree(self) -> None: manifest = json.loads((self.root / "docs" / "versions.json").read_text(encoding="utf-8")) - self.assertEqual("1.3.0", __version__) + self.assertEqual("1.3.1", __version__) self.assertEqual(__version__, manifest["current"]) self.assertEqual(f"v{__version__}", manifest["versions"][0]["source_ref"]) pyproject = (self.root / "pyproject.toml").read_text(encoding="utf-8") self.assertIn('name = "docsprout"', pyproject) - self.assertIn('version = "1.3.0"', pyproject) + self.assertIn('version = "1.3.1"', pyproject) self.assertIn('license = "MIT"', pyproject) self.assertIn('name = "DocSprout contributors"', pyproject) self.assertNotIn('Development Status :: 3 - Alpha', pyproject) @@ -295,7 +295,7 @@ def test_qualification_document_matches_the_ci_contract(self) -> None: self.assertIn("wheel", qualification) self.assertIn("sdist", qualification) self.assertIn("## Manual browser/keyboard matrix", qualification) - self.assertIn("# Qualification evidence for DocSprout v1.3.0", qualification) + self.assertIn("# Qualification evidence for DocSprout v1.3.1", qualification) self.assertIn("Browser automation is not part of CI", qualification) self.assertIn("ruff check", qualification) self.assertIn("ruff check .", ci) @@ -348,7 +348,7 @@ def test_build_guide_documents_options_archives_and_doctor(self) -> None: guide = (self.root / "docs" / "building.md").read_text(encoding="utf-8") readme = (self.root / "README.md").read_text(encoding="utf-8") - self.assertIn("docsprout build --release 1.3.0 --offline-archive", guide) + self.assertIn("docsprout build --release 1.3.1 --offline-archive", guide) self.assertIn("`--root `", guide) self.assertIn("Status: preview-ready", guide) self.assertIn("docs/building.md", readme) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 643eb66..edc5aaa 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -20,7 +20,7 @@ def test_reusable_workflow_supports_versioned_and_single_version_sites(self) -> def test_reusable_workflow_installs_the_pinned_docsprout_release(self) -> None: workflow = (self.root / ".github" / "workflows" / "publish-docs.yml").read_text(encoding="utf-8") - self.assertIn("git+https://github.com/ikelaiah/docsprout.git@v1.3.0", workflow) + self.assertIn("git+https://github.com/ikelaiah/docsprout.git@v1.3.1", workflow) self.assertNotIn("python -m pip install .", workflow) def test_workflows_use_checkout_v7_node_24_major(self) -> None: @@ -51,7 +51,7 @@ def test_examples_pin_the_release_and_select_the_intended_mode(self) -> None: single = (self.root / "examples" / "single-version" / ".github" / "workflows" / "documentation.yml").read_text(encoding="utf-8") historical = (self.root / "examples" / "historical" / ".github" / "workflows" / "documentation.yml").read_text(encoding="utf-8") - self.assertIn("publish-docs.yml@v1.3.0", single) + self.assertIn("publish-docs.yml@v1.3.1", single) self.assertIn("versioned: false", single) - self.assertIn("publish-docs.yml@v1.3.0", historical) + self.assertIn("publish-docs.yml@v1.3.1", historical) self.assertNotIn("versioned: false", historical)