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
8 changes: 8 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ jobs:
with:
persist-credentials: false
fetch-depth: 0
- name: Observe latest stable release for documentation
if: matrix.lane == 'docs'
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 -B Scripts/documentation_release.py --output .build/handoff/pages/latest-release.json
- run: python3 -B Scripts/ci.py --lane '${{ matrix.lane }}'
env:
SWIFT_JSON_RELEASE_METADATA: ${{ github.workspace }}/.build/handoff/pages/latest-release.json
- name: Public composition and reference imports
if: matrix.lane == 'native'
run: python3 -B Scripts/ci.py --lane composition
Expand All @@ -47,6 +54,7 @@ jobs:
.build/handoff/docc/*-diagnostics.json
.build/handoff/docc/*.log
.build/handoff/pages/receipt.json
.build/handoff/pages/latest-release.json
.build/handoff/pages/site.MANIFEST.sha256
.build/handoff/pages/*.log
- name: Package qualified documentation site
Expand Down
26 changes: 26 additions & 0 deletions Documentation/DocumentationTooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ The manifest and receipt preserve file-level and archive-level digests.

## Publish and preview the website

Each Native CI documentation build reads GitHub's
[latest published release endpoint](https://docs.github.com/en/rest/releases/releases#get-the-latest-release)
once, using its existing read-only repository token. Drafts and prereleases are
rejected. The exact response is retained as
`.build/handoff/pages/latest-release.json`; its SHA-256, release ID, tag and
publication time are recorded in the Pages receipt and public `build-info.json`.
The landing page derives its stable version, tagged source link and any available
compiled DocC download from that snapshot. A failed request or invalid configured
snapshot fails the CI build instead of reusing a stale release label. This lookup
adds no network behavior to the libraries or the rendered website.

The references continue to track the checked-out source, independently of the
latest stable release. The source version comes from the existing CLI release
version declaration; the landing heading has no manually maintained version.
Release metadata refreshes whenever the documentation lane runs. For a local
build with the same observed release input, fetch or reuse a retained snapshot:

```sh
python3 -B Scripts/documentation_release.py --output .build/handoff/pages/latest-release.json
SWIFT_JSON_RELEASE_METADATA=.build/handoff/pages/latest-release.json python3 -B Scripts/ci.py --lane docs
```

An unconfigured local preview remains offline and uses a version-neutral latest
release link. An explicitly configured missing or malformed snapshot is an error.
No release tag or frozen documentation archive is changed by this metadata step.

The docs lane uses native `docc process-archive transform-for-static-hosting` to
prepare each qualified archive under `/Swift-JSON/<product>`. A shared landing
page links all twelve product references and guides. The supporting DSL reference
Expand Down
49 changes: 39 additions & 10 deletions Scripts/build_documentation_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
import urllib.parse

from documentation_catalog import PRODUCTS, CATALOGS
from documentation_release import REPOSITORY, load_metadata
from preflight import collect

ROOT = pathlib.Path(__file__).resolve().parents[1]
REPOSITORY = 'https://github.com/ThorFuchs/Swift-JSON'
BASE_PATH = '/Swift-JSON'
SITE_URL = 'https://thorfuchs.github.io' + BASE_PATH + '/'
SOURCE_VERSION = '1.7.0'
STABLE_VERSION = '1.6.1'
GUIDES = {'JSON': ('Exact values and native coding', 'exactvaluesandcoding'),
'JSONSchema': ('Offline schema validation', 'offlinevalidation'),
'JSONSchemaExamples': ('Verified exact candidates', 'verifiedcandidates'),
Expand All @@ -43,7 +41,7 @@
'JSONHTML': 'Exact standalone HTML with offline disclosure/search and native-byte copy/download.',
'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.',
'JSONNavigation': 'Exact typed DFS with physical and virtual scopes, explicit coverage and one cumulative budget.',
'JSONInspection': 'Array evidence, complete source-attached snapshots, tree views and strict sparse tables with finite reports.',
'JSONInspection': 'Array evidence, source-attached snapshots, tree and sparse-table views, exact sets and matrices, and directed graphs with finite reports.',
'JSONTransform': 'Exact query plans and guarded pull executions with cumulative finite limits.',
'JSONValueDSL': 'Trusted authored exact objects and arrays with finite finalization.',
'JSONSchemaDSL': 'Trusted authored schema declarations using native compilation and evaluation.'}
Expand Down Expand Up @@ -72,7 +70,25 @@ def source_commit():
return subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()


def landing(commit):
def source_version():
"""Read the source line's existing version authority, never the latest release."""
source = (ROOT / 'Sources/JSONCLIEngine/CLIReleaseVersion.swift').read_text()
versions = re.findall(r'^\s*package static let value = "([^"]+)"\s*$', source, re.MULTILINE)
if len(versions) != 1:
raise ValueError('Missing or ambiguous CLI source version')
return versions[0]


def observed_release():
path = os.environ.get('SWIFT_JSON_RELEASE_METADATA')
if path:
return load_metadata(path)
if os.environ.get('GITHUB_ACTIONS') == 'true':
raise ValueError('CI documentation requires an observed latest-release snapshot')
return None


def landing(commit, release=None):
cards = []
for product in PRODUCTS:
route = BASE_PATH + '/' + product + '/documentation/' + product.lower() + '/'
Expand All @@ -84,6 +100,15 @@ def landing(commit):
<a href="{guide_route}">{title}</a></article>''')
source = (f'<a href="{REPOSITORY}/commit/{commit}"><code>{commit[:12]}</code></a>'
if commit else 'Local preview')
if release:
stable = (f'Latest stable: <a href="{html.escape(release["release_url"], quote=True)}">'
f'Swift-JSON {html.escape(release["version"])}</a>. For a frozen reference, use its ')
if release['documentation_url']:
stable += f'<a href="{html.escape(release["documentation_url"], quote=True)}">compiled DocC catalogs</a> or '
stable += f'<a href="{html.escape(release["source_url"], quote=True)}">tagged source documentation</a>.'
else:
stable = (f'<a href="{REPOSITORY}/releases/latest">Latest stable release and versioned documentation</a>. '
'This local preview has no observed release metadata.')
return f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Swift-JSON Documentation</title>
Expand Down Expand Up @@ -111,11 +136,11 @@ def landing(commit):
<a class="skip" href="#content">Skip to documentation</a>
<header><nav class="container" aria-label="Main navigation"><a class="brand" href="{BASE_PATH}/">Swift-JSON</a>
<div class="nav-links"><a href="{REPOSITORY}#installation">Get started</a><a href="{REPOSITORY}">GitHub</a><a href="{REPOSITORY}/issues">Support</a></div></nav></header>
<main id="content" class="container"><div class="eyebrow">Unreleased {SOURCE_VERSION} documentation</div>
<main id="content" class="container"><div class="eyebrow">Swift-JSON documentation</div>
<h1>Exact JSON. Clear contracts.</h1>
<p class="intro">Keep decimal values exact, Unicode scalar keys distinct and resource use explicit. Explore {len(PRODUCTS)} Swift libraries that carry those semantics from input bytes to validated, transformed and encoded output.</p>
<p class="baseline">Swift 6.4+ · Swift language mode 6 · macOS/SDK 27+ · No package dependencies</p>
<p class="baseline">Development main includes JSONInspection, JSONTransform and the optional authored DSLs; this source is prepared as 1.7.0. Stable <a href="{REPOSITORY}/releases/tag/{STABLE_VERSION}">{STABLE_VERSION}</a> has eight library catalogs; use its matching compiled DocC assets or <a href="{REPOSITORY}/tree/{STABLE_VERSION}/Sources">tagged source documentation</a>.</p>
<p class="baseline">{stable}</p>
<section class="grid" aria-label="Product references">{''.join(cards)}</section>
<section class="details"><h2>Start with the contract</h2>
<p>Read the <a href="{REPOSITORY}#readme">package overview and executable examples</a>, then choose a reference above. The <a href="{REPOSITORY}/blob/main/Documentation/README.md">behavior contracts</a> explain exact numbers, scalar identity, offline schema evaluation, cumulative budgets and stream completion.</p>
Expand Down Expand Up @@ -231,6 +256,8 @@ def main():
try:
if receipt['environment']['status'] != 'passed':
raise ValueError('Supported native toolchain unavailable')
release = observed_release()
receipt['latest_release'] = release
catalogs = ROOT / '.build/handoff/docc'
# Verify the exact compiler outputs already packaged by the DocC lane.
manifest = catalogs / 'compiled-catalogs.MANIFEST.sha256'
Expand Down Expand Up @@ -281,7 +308,7 @@ def main():
(site / product / 'index.html').write_text(f'<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="refresh" content="0; url={route}"><link rel="canonical" href="https://thorfuchs.github.io{route}"><title>{product} · Swift-JSON</title><p><a href="{route}">Open the {product} reference</a>.</p></html>')
commit = source_commit()
receipt['source_commit'] = commit
(site / 'index.html').write_text(landing(commit))
(site / 'index.html').write_text(landing(commit, release))
(site / '.nojekyll').touch()
(site / '404.html').write_text(f'<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Page not found · Swift-JSON</title><h1>Page not found</h1><p><a href="{BASE_PATH}/">Open Swift-JSON documentation</a> or <a href="{REPOSITORY}">browse the source</a>.</p></html>')
(site / 'build-info.json').write_text(json.dumps({
Expand All @@ -290,8 +317,10 @@ def main():
'swift': receipt['environment']['swift'], 'sdk': receipt['environment']['sdk'],
'macos': receipt['environment']['macos'], 'swift_language_mode': '6',
'products': list(PRODUCTS), 'catalogs': list(CATALOGS), 'channel': 'development',
'source_version': SOURCE_VERSION, 'latest_stable_observed': STABLE_VERSION,
'stable_release': REPOSITORY + '/releases/tag/' + STABLE_VERSION}, indent=2) + '\n')
'source_version': source_version(),
'latest_stable_observed': release['tag'] if release else None,
'stable_release': release['release_url'] if release else None,
'latest_release': release}, indent=2) + '\n')
routes = sorted(p.relative_to(site).parent.as_posix() + '/' for p in site.rglob('index.html'))
urls = [SITE_URL if route == './' else SITE_URL + urllib.parse.quote(route, safe='/') for route in routes]
(site / 'sitemap.xml').write_text('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + ''.join('<url><loc>' + html.escape(url) + '</loc></url>\n' for url in urls) + '</urlset>\n')
Expand Down
106 changes: 106 additions & 0 deletions Scripts/documentation_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Observe and replay GitHub's latest published stable release for documentation."""
import argparse
import hashlib
import json
import os
import pathlib
import re
import sys
import urllib.parse
import urllib.request

REPOSITORY = 'https://github.com/ThorFuchs/Swift-JSON'
API_URL = 'https://api.github.com/repos/ThorFuchs/Swift-JSON/releases/latest'
MAX_METADATA_BYTES = 2 * 1024 * 1024


class NoRedirect(urllib.request.HTTPRedirectHandler):
"""Keep the optional read token on the single approved GitHub API endpoint."""
def redirect_request(self, request, response, code, message, headers, url):
return None


def release_metadata(data):
"""Validate external metadata and advertise only an actual uploaded DocC asset."""
if not isinstance(data, dict):
raise ValueError('Expected a GitHub release object')
tag = data.get('tag_name')
if (not isinstance(tag, str) or not re.fullmatch(
r'v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?', tag)):
raise ValueError('Latest release must have a stable semantic version tag')
if (data.get('draft') is not False or data.get('prerelease') is not False
or not isinstance(data.get('published_at'), str) or not data['published_at']
or type(data.get('id')) is not int or data['id'] <= 0):
raise ValueError('Latest release must be published, non-draft and non-prerelease')
encoded_tag = urllib.parse.quote(tag, safe='')
release_url = REPOSITORY + '/releases/tag/' + encoded_tag
if data.get('html_url') != release_url:
raise ValueError('Release URL must belong to this repository and tag')
version = tag.removeprefix('v')
asset_name = 'Swift-JSON-' + version + '-DocC.tar.gz'
assets = data.get('assets')
if not isinstance(assets, list) or any(not isinstance(a, dict) for a in assets):
raise ValueError('Malformed release asset inventory')
matches = [a for a in assets if a.get('name') == asset_name]
if len(matches) > 1:
raise ValueError('Ambiguous compiled DocC asset')
documentation_url = None
if matches:
asset = matches[0]
expected_url = REPOSITORY + '/releases/download/' + encoded_tag + '/' + urllib.parse.quote(asset_name, safe='')
if (asset.get('state') != 'uploaded' or type(asset.get('size')) is not int
or asset['size'] <= 0 or asset.get('browser_download_url') != expected_url):
raise ValueError('Compiled DocC asset is unavailable or has a foreign URL')
documentation_url = expected_url
return {'release_id': data['id'], 'tag': tag, 'version': version,
'published_at': data['published_at'], 'release_url': release_url,
'source_url': REPOSITORY + '/tree/' + encoded_tag + '/Sources',
'documentation_url': documentation_url}


def decode_metadata(raw):
if len(raw) > MAX_METADATA_BYTES:
raise ValueError('Release metadata exceeds the 2 MiB build-input bound')
result = release_metadata(json.loads(raw))
return dict(result, api_url=API_URL, metadata_sha256=hashlib.sha256(raw).hexdigest())


def load_metadata(path):
with pathlib.Path(path).open('rb') as stream:
return decode_metadata(stream.read(MAX_METADATA_BYTES + 1))


def fetch_metadata(output):
headers = {'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2026-03-10',
'User-Agent': 'Swift-JSON-documentation-build'}
token = os.environ.get('GITHUB_TOKEN')
if token:
headers['Authorization'] = 'Bearer ' + token
request = urllib.request.Request(API_URL, headers=headers)
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
if response.status != 200 or response.url != API_URL:
raise ValueError('Unexpected latest-release response or redirect')
raw = response.read(MAX_METADATA_BYTES + 1)
result = decode_metadata(raw)
output = pathlib.Path(output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(raw)
return result


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', required=True, help='Ignored JSON snapshot, retained with CI receipts')
args = parser.parse_args()
try:
print(json.dumps(fetch_metadata(args.output), indent=2))
except (OSError, ValueError) as error:
print('DOCUMENTATION_RELEASE failed: ' + str(error), file=sys.stderr)
return 1
return 0


if __name__ == '__main__':
sys.exit(main())
Loading