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
9 changes: 9 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ jobs:
broken_symlink_check_enabled: true
shell_check_enabled: true
yamllint_check_enabled: true

docs-build-tests:
name: docs-build-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Run scripts test suite
run: python3 -m unittest discover -s scripts -p 'test_*.py' -v
17 changes: 11 additions & 6 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ python3 -m http.server 8123 --directory .build-output
Then in another terminal:

```bash
open http://localhost:8123/main/documentation/
open http://localhost:8123/latest/documentation/
```

Serve from `.build-output` (the parent), not `.build-output/main`: the build
bakes a `/main/` hosting base path into every asset URL, so the `/main/` prefix
must map to the `main/` directory. This example avoids port 8000, which can be
commonly used by other apps or examples.
Serve from `.build-output` (the parent), not `.build-output/latest`: the build
bakes a `/latest/` hosting base path into every asset URL, so the `/latest/`
prefix must map to the `latest/` directory. This example avoids port 8000,
which can be commonly used by other apps or examples.

> Note: Ensure that you have a version of Swift available that will build the
> content that you point at in sources.json. For latest development tree builds,
> that typically requires either the latest Xcode beta or a Swift nightly development
> build to support Swift, SwiftPM, swift-testing, and other core projects.

## Navigation manifest (combined sidebar curation)

Expand All @@ -31,7 +36,7 @@ from the main page's module list as well as the sidebar. Each entry names a
in the merged index must be either placed in a group or listed under `hidden` —
`build_docs.py` validates and applies this automatically (the
`navigator-curation` build step), and fails the build on any mismatch or
uncovered module. See `../hacking-index-json.md` for the underlying mechanics.
uncovered module. See `hacking-index-json.md` for the underlying mechanics.

A group's `title` is optional — omit it (or set it to `null`) for a headerless
group whose modules render with no section label in either the sidebar or the
Expand Down
10 changes: 1 addition & 9 deletions scripts/build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,10 @@ def validate_sources(config):
if "version" not in config:
errors.append("Top-level 'version' field is missing")
elif not isinstance(config["version"], dict):
errors.append(
"Top-level 'version' field must be an object with 'slug' and "
"'descriptive-name'"
)
errors.append("Top-level 'version' field must be an object with 'slug'")
else:
if not config["version"].get("slug"):
errors.append("Top-level 'version' object is missing 'slug'")
# 'descriptive-name' is validated and recorded in build-manifest.json but
# not otherwise consumed by the build; reserved for a future narrative use
# (e.g. re-introducing a landing-page blurb — see hacking-synthesized-landing-page.md).
if not config["version"].get("descriptive-name"):
errors.append("Top-level 'version' object is missing 'descriptive-name'")

if "sources" not in config:
errors.append("Top-level 'sources' field is missing")
Expand Down
53 changes: 13 additions & 40 deletions scripts/hacking-synthesized-landing-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
> **Status: not currently wired up.** `set_version_paragraph()` and its call
> from `_finalize_combined_archive()` have been removed from the build
> scripts — the combined archive's landing page no longer gets a "Swift
> version: X" paragraph injected. `sources.json`'s `version.descriptive-name`
> is still validated and recorded in `build-manifest.json`, but nothing reads
> it to alter documentation content. The mechanics below (verified against
> version: X" paragraph injected. The mechanics below (verified against
> `swift-docc` source) remain accurate if this is revisited.

Companion to `hacking-index-json.md` (sidebar/navigator). This file covers the
Expand Down Expand Up @@ -101,43 +99,18 @@ This renders as normal running prose **above the Topics section** and below
the title/role-heading — it is body content, not the short one-line
`abstract` teaser (a separate, also-currently-absent top-level key).

## Where this lives in our pipeline

`scripts/curate_navigator.py` → `set_version_paragraph(archive_path, text)`:
reads `data/documentation.json`, unconditionally replaces
`doc["primaryContentSections"]` with a single content section containing one
paragraph, writes atomically. No-op if the file is missing. Because it
replaces the array wholesale rather than appending, calling it repeatedly
(e.g. re-running the build) is idempotent — it does not accumulate
paragraphs.

Called from `scripts/build_docs.py` → `_finalize_combined_archive()`,
**unconditionally** (not gated on `navigation.json` existing, unlike
`curate_navigator()`/topic-section curation), right after `merge_archives()`
succeeds and before the static-hosting transform:

```python
descriptive_name = (version or {}).get("descriptive-name")
if descriptive_name:
set_version_paragraph(combined_output, f"Swift version: {descriptive_name}")
```

`version` is the full `sources.json` `"version"` object
(`{"slug": ..., "descriptive-name": ...}`); the text is built here in
`build_docs.py`, not inside `curate_navigator.py`, so `set_version_paragraph`
stays a generic "set this text as the landing page body" primitive.

### Why it must run before the static-hosting transform
## Ordering constraint: run before the static-hosting transform

`transform_static_hosting()` runs `docc process-archive
transform-for-static-hosting`, which produces a **fresh archive** at a temp
path and then replaces the original wholesale (`shutil.rmtree` +
`shutil.move`). Whatever is in `data/documentation.json` at the time that
command runs is what gets baked into the final per-route HTML; editing the
JSON afterward doesn't help because the static-hosting archive may not carry
the same `data/*.json` structure forward. Same ordering constraint as
`curate_navigator()`'s landing-page section rewriting — see
`hacking-index-json.md`.
Any script that edits `data/documentation.json` — a future reimplementation
of this recipe included — must run before `transform_static_hosting()`.
That function runs `docc process-archive transform-for-static-hosting`,
which produces a **fresh archive** at a temp path and then replaces the
original wholesale (`shutil.rmtree` + `shutil.move`). Whatever is in
`data/documentation.json` at the time that command runs is what gets baked
into the final per-route HTML; editing the JSON afterward doesn't help
because the static-hosting archive may not carry the same `data/*.json`
structure forward. Same ordering constraint as `curate_navigator()`'s
landing-page section rewriting — see `hacking-index-json.md`.

## Extending this further

Expand Down Expand Up @@ -172,7 +145,7 @@ published at <https://heckj.github.io/DocCArchive/>.
the title) — also omitted when empty on the synthesized landing page. Don't
conflate it with `primaryContentSections`; this doc's approach targets body
prose, not the teaser line.
- **Ordering vs. navigator curation:** `set_version_paragraph()` and
- **Ordering vs. navigator curation:** a landing-page-body rewrite and
`curate_navigator()` both rewrite `data/documentation.json` but touch
disjoint keys (`primaryContentSections` vs. `topicSections`/`references`),
so call order between them doesn't matter — verified by both being simple
Expand Down
3 changes: 1 addition & 2 deletions scripts/sources.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"version": {
"slug": "main",
"descriptive-name": "prototype"
"slug": "latest"
},
"sources": [
{
Expand Down
23 changes: 9 additions & 14 deletions scripts/test_build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _validate(config):

def _wrap(entry):
return {
"version": {"slug": "main", "descriptive-name": "prototype"},
"version": {"slug": "main"},
"sources": [entry],
}

Expand All @@ -67,11 +67,11 @@ def _config(self, version):
}

def test_valid_version_object(self):
output = _validate(self._config({"slug": "main", "descriptive-name": "prototype"}))
output = _validate(self._config({"slug": "main"}))
self.assertIsNone(output)

def test_version_missing_is_rejected(self):
config = self._config({"slug": "main", "descriptive-name": "prototype"})
config = self._config({"slug": "main"})
del config["version"]
output = _validate(config)
self.assertIsNotNone(output)
Expand All @@ -83,15 +83,10 @@ def test_version_as_plain_string_is_rejected(self):
self.assertIn("version", output)

def test_version_missing_slug_is_rejected(self):
output = _validate(self._config({"descriptive-name": "prototype"}))
output = _validate(self._config({}))
self.assertIsNotNone(output)
self.assertIn("slug", output)

def test_version_missing_descriptive_name_is_rejected(self):
output = _validate(self._config({"slug": "main"}))
self.assertIsNotNone(output)
self.assertIn("descriptive-name", output)


class ValidateArchiveType(unittest.TestCase):
def test_minimal_valid(self):
Expand Down Expand Up @@ -1632,7 +1627,7 @@ def _children_of(archive, lang="swift"):

class ValidateNavigation(unittest.TestCase):
SOURCES = {
"version": {"slug": "main", "descriptive-name": "prototype"},
"version": {"slug": "main"},
"sources": [{"id": "a"}, {"id": "b"}],
}

Expand Down Expand Up @@ -2298,7 +2293,7 @@ def test_unlisted_module_raises(self):


class AutoArchiveVersionSlug(unittest.TestCase):
"""_auto_archive must resolve the {slug, descriptive-name} version object."""
"""_auto_archive must resolve the {slug} version object."""

def _sources_path(self, tmp_path):
scripts_dir = tmp_path / "scripts"
Expand All @@ -2310,7 +2305,7 @@ def test_uses_slug_from_version_object(self):
tmp_path = Path(tmp)
sources_path = self._sources_path(tmp_path)
sources = {
"version": {"slug": "main", "descriptive-name": "prototype"},
"version": {"slug": "main"},
"sources": [],
}
index_dir = tmp_path / ".build-output" / "main" / "index"
Expand All @@ -2325,7 +2320,7 @@ def test_missing_slug_returns_none(self):
tmp_path = Path(tmp)
sources_path = self._sources_path(tmp_path)
sources = {
"version": {"descriptive-name": "prototype"},
"version": {},
"sources": [],
}
self.assertIsNone(validate_navigation_cli._auto_archive(sources, sources_path))
Expand All @@ -2352,7 +2347,7 @@ def _setup(self, tmp, nav, sources, archive_modules=None):
return argv

SOURCES = {
"version": {"slug": "main", "descriptive-name": "prototype"},
"version": {"slug": "main"},
"sources": [{"id": "a"}, {"id": "b"}],
}

Expand Down