diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 0a5876c0..310c9b1a 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -27,7 +27,7 @@ concurrency: jobs: deploy: permissions: - contents: write + contents: read pages: write id-token: write environment: @@ -41,26 +41,6 @@ jobs: fetch-depth: 0 ref: main - - name: Add release to versioned documentation - if: github.event_name == 'release' && !github.event.release.prerelease - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: | - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then - echo "::error::Release tags must use the format vX.Y.Z with an optional prerelease suffix (e.g. v1.2.3-alpha)" - exit 1 - fi - jq --arg tag "$RELEASE_TAG" '.versions = [$tag] + (.versions - [$tag])' \ - api-docs/versions.json > api-docs/versions.json.tmp - mv api-docs/versions.json.tmp api-docs/versions.json - git add api-docs/versions.json - if ! git diff --cached --quiet; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "docs: add $RELEASE_TAG to the versioned docs" - git push origin HEAD:main - fi - - name: Setup Pages uses: actions/configure-pages@v5 diff --git a/api-docs/versions.json b/api-docs/versions.json deleted file mode 100644 index aab48ff7..00000000 --- a/api-docs/versions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "versions": [ - "v0.38.0", - "v0.37.0" - ] -} diff --git a/scripts/generate_versioned_api_docs.py b/scripts/generate_versioned_api_docs.py index db2c4118..e63ec460 100644 --- a/scripts/generate_versioned_api_docs.py +++ b/scripts/generate_versioned_api_docs.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build a GitHub Pages site from the explicitly selected GitHub releases.""" +"""Build a GitHub Pages site from published GitHub releases at or above MIN_VERSION.""" import io import json @@ -17,42 +17,53 @@ ROOT = Path(__file__).resolve().parents[1] -RELEASES_URL = "https://api.github.com/repos/contentauth/c2pa-python/releases/tags" +RELEASES_URL = "https://api.github.com/repos/contentauth/c2pa-python/releases" +# Oldest release to include in the versioned docs; override with C2PA_DOCS_MIN_VERSION. +MIN_VERSION = os.environ.get("C2PA_DOCS_MIN_VERSION", "v0.37.0") TAG_PATTERN = re.compile(r"v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9][A-Za-z0-9.-]*)?") -def load_versions(path: Path) -> list[str]: - config = json.loads(path.read_text(encoding="utf-8")) - versions = config.get("versions") if isinstance(config, dict) else None - if not isinstance(versions, list) or not versions: - raise ValueError("Documentation versions must be a non-empty list.") - for tag in versions: - if not isinstance(tag, str) or not TAG_PATTERN.fullmatch(tag): - raise ValueError("Documentation versions must be release tags such as v0.38.0.") - if len(set(versions)) != len(versions): - raise ValueError("Documentation versions must not contain duplicate tags.") - return versions +def parse_tag(tag: str) -> tuple[int, int, int]: + return tuple(int(part) for part in tag.lstrip("v").split("-")[0].split(".")) -def verify_release(tag: str) -> None: +def load_versions(min_version: str = MIN_VERSION) -> list[str]: + """List published, non-prerelease GitHub releases at or above min_version, newest first.""" + if not TAG_PATTERN.fullmatch(min_version): + raise ValueError("Minimum version must be a release tag such as v0.37.0.") + minimum = parse_tag(min_version) headers = {"Accept": "application/vnd.github+json"} token = os.environ.get("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" - response = requests.get( - f"{RELEASES_URL}/{tag}", headers=headers, timeout=30, allow_redirects=False - ) - if response.status_code != 200: - raise RuntimeError( - f"Cannot verify GitHub release {tag}: HTTP {response.status_code}." + versions = [] + page = 1 + while True: + response = requests.get( + RELEASES_URL, headers=headers, timeout=30, + params={"per_page": 100, "page": page}, ) - release = response.json() - if ( - not isinstance(release, dict) - or release.get("tag_name") != tag - or release.get("draft") is not False - ): - raise ValueError(f"{tag} is not a published GitHub release.") + if response.status_code != 200: + raise RuntimeError(f"Cannot list GitHub releases: HTTP {response.status_code}.") + releases = response.json() + if not releases: + break + for release in releases: + tag = release.get("tag_name", "") + if ( + release.get("draft") + or release.get("prerelease") + or not TAG_PATTERN.fullmatch(tag) + or "-" in tag + or parse_tag(tag) < minimum + ): + continue + versions.append(tag) + page += 1 + versions = sorted(set(versions), key=parse_tag, reverse=True) + if not versions: + raise ValueError(f"No published releases found at or above {min_version}.") + return versions def export_sources(root: Path, tag: str, destination: Path) -> None: @@ -92,9 +103,7 @@ def export_sources(root: Path, tag: str, destination: Path) -> None: def build_site() -> Path: docs = ROOT / "api-docs" - versions = load_versions(docs / "versions.json") - for tag in versions: - verify_release(tag) + versions = load_versions() build = docs / "_build" build.mkdir(exist_ok=True)