From 6cbb2ecec17902edfcde557558bc30fa1a7297de Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 18:18:29 -0300 Subject: [PATCH] chore: remove duplicated release history sync --- actions/sync-release-history/action.yml | 96 ------------ scripts/sync_release_history.py | 192 ------------------------ tests/test_sync_release_history.py | 83 ---------- 3 files changed, 371 deletions(-) delete mode 100644 actions/sync-release-history/action.yml delete mode 100755 scripts/sync_release_history.py delete mode 100644 tests/test_sync_release_history.py diff --git a/actions/sync-release-history/action.yml b/actions/sync-release-history/action.yml deleted file mode 100644 index 548f82b..0000000 --- a/actions/sync-release-history/action.yml +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Synchronize release history -description: Generate a reviewable documentation PR from a successful PublicationVerification v1. - -inputs: - prepared-path: - description: PreparedRelease v1 JSON path. - required: true - verification-path: - description: Successful PublicationVerification v1 JSON path. - required: true - documentation-repository: - description: Documentation repository in owner/name form. - required: true - app-id: - description: GitHub App id used for the documentation-scoped token. - required: true - app-private-key: - description: GitHub App private key. - required: true - -outputs: - pull-request-number: - description: Documentation synchronization PR number. - value: ${{ steps.pull-request.outputs.pull-request-number }} - pull-request-url: - description: Documentation synchronization PR URL. - value: ${{ steps.pull-request.outputs.pull-request-url }} - -runs: - using: composite - steps: - - id: docs-repository - name: Resolve documentation repository - shell: bash - env: - DOCUMENTATION_REPOSITORY: ${{ inputs.documentation-repository }} - run: | - set -euo pipefail - echo "owner=${DOCUMENTATION_REPOSITORY%%/*}" >> "${GITHUB_OUTPUT}" - echo "name=${DOCUMENTATION_REPOSITORY#*/}" >> "${GITHUB_OUTPUT}" - - - id: app-token - name: Create documentation token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 - with: - app-id: ${{ inputs.app-id }} - private-key: ${{ inputs.app-private-key }} - owner: ${{ steps.docs-repository.outputs.owner }} - repositories: ${{ steps.docs-repository.outputs.name }} - permission-contents: write - permission-pull-requests: write - - - name: Checkout documentation - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: ${{ inputs.documentation-repository }} - token: ${{ steps.app-token.outputs.token }} - persist-credentials: false - path: release-docs - - - id: render - name: Render release history - shell: bash - env: - PREPARED_PATH: ${{ inputs.prepared-path }} - VERIFICATION_PATH: ${{ inputs.verification-path }} - run: | - set -euo pipefail - python3 "${GITHUB_ACTION_PATH}/../../scripts/sync_release_history.py" \ - --prepared "${PREPARED_PATH}" \ - --verification "${VERIFICATION_PATH}" \ - --docs-root release-docs \ - > "${RUNNER_TEMP}/release-history-sync.json" - - - id: pull-request - name: Create or update documentation pull request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ steps.app-token.outputs.token }} - path: release-docs - commit-message: 'docs: synchronize LibreSign release history' - committer: GitHub - author: github-workflows bot - signoff: true - branch: automated/libresign-release-history - delete-branch: true - title: 'docs: synchronize LibreSign release history' - body: | - Automated release-history synchronization after successful PublicationVerification v1. - - Release text is generated from the canonical per-major changelog in LibreSign/libresign. Do not edit generated release text manually in this repository. - add-paths: | - developer_manual/release-history/** diff --git a/scripts/sync_release_history.py b/scripts/sync_release_history.py deleted file mode 100755 index e71504d..0000000 --- a/scripts/sync_release_history.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path - -VERSION_RE = re.compile(r"^(?P\d+)\.(?P\d+)\.(?P\d+)(?:-(?P
[A-Za-z0-9.-]+))?$")
-HEADING_RE = re.compile(r"^(#{2,3})\s+(.+?)\s*$")
-LINK_RE = re.compile(r"\[([^\]]+)\]\((https?://[^)]+)\)")
-CODE_RE = re.compile(r"`([^`]+)`")
-RST_HEADER = [
-    ".. SPDX-FileCopyrightText: 2026 LibreCode coop and contributors",
-    ".. SPDX-License-" + "Identifier: AGPL-3.0-or-later",
-    "",
-]
-
-
-def load_json(path: Path) -> dict[str, object]:
-    payload = json.loads(path.read_text(encoding="utf-8"))
-    if not isinstance(payload, dict):
-        raise ValueError(f"{path} must contain a JSON object")
-    return payload
-
-
-def inline_rst(text: str) -> str:
-    text = CODE_RE.sub(lambda match: f"``{match.group(1)}``", text)
-    text = LINK_RE.sub(lambda match: f"`{match.group(1)} <{match.group(2)}>`_", text)
-    return text
-
-
-def markdown_section_to_rst(section: str, version: str) -> str:
-    lines = section.strip().splitlines()
-    if not lines:
-        raise ValueError("changelog section must not be empty")
-
-    output: list[str] = [*RST_HEADER, ".. This file is generated from LibreSign/libresign release history.", ""]
-    first_heading_seen = False
-    for raw in lines:
-        match = HEADING_RE.match(raw)
-        if match:
-            level, title = match.groups()
-            title = inline_rst(title)
-            if level == "##":
-                if first_heading_seen:
-                    raise ValueError("release section contains more than one version heading")
-                first_heading_seen = True
-                if not title.startswith(version):
-                    raise ValueError(f"release heading does not start with version {version}")
-                output.extend([title, "=" * len(title), ""])
-            else:
-                output.extend([title, "-" * len(title), ""])
-            continue
-
-        if raw.startswith("- "):
-            output.append("* " + inline_rst(raw[2:]))
-        else:
-            output.append(inline_rst(raw))
-
-    if not first_heading_seen:
-        raise ValueError("release section does not contain a version heading")
-    return "\n".join(output).rstrip() + "\n"
-
-
-def release_filename(version: str) -> str:
-    if VERSION_RE.fullmatch(version) is None:
-        raise ValueError(f"unsupported release version: {version}")
-    return f"{version}.rst"
-
-
-def version_key(version: str) -> tuple[int, int, int, int, int, str]:
-    match = VERSION_RE.fullmatch(version)
-    if match is None:
-        raise ValueError(f"unsupported release version: {version}")
-
-    prerelease = match.group("pre")
-    if prerelease is None:
-        stage_rank = 4
-        sequence = 0
-        fallback = ""
-    else:
-        parts = prerelease.split(".", 1)
-        stage_rank = {"alpha": 1, "beta": 2, "rc": 3}.get(parts[0].lower(), 0)
-        sequence = int(parts[1]) if len(parts) == 2 and parts[1].isdigit() else 0
-        fallback = prerelease
-
-    return (
-        int(match.group("major")),
-        int(match.group("minor")),
-        int(match.group("patch")),
-        stage_rank,
-        sequence,
-        fallback,
-    )
-
-
-def render_major_index(major: int, versions: list[str]) -> str:
-    ordered = sorted(versions, key=version_key, reverse=True)
-    title = f"LibreSign {major}"
-    body = [
-        *RST_HEADER,
-        ".. This file is generated from LibreSign release history. Do not edit release text here manually.",
-        "",
-        title,
-        "=" * len(title),
-        "",
-        ".. toctree::",
-        "   :maxdepth: 1",
-        "",
-    ]
-    body.extend(f"   {version}" for version in ordered)
-    return "\n".join(body) + "\n"
-
-
-def render_root_index(majors: list[int]) -> str:
-    title = "Release history"
-    body = [
-        *RST_HEADER,
-        ".. This file is generated. Release text is sourced from LibreSign/libresign per-major changelogs.",
-        "",
-        title,
-        "=" * len(title),
-        "",
-        "Published LibreSign release history is generated after publication verification succeeds.",
-        "",
-        ".. toctree::",
-        "   :maxdepth: 2",
-        "",
-    ]
-    body.extend(f"   LibreSign {major} <{major}/index>" for major in sorted(majors, reverse=True))
-    return "\n".join(body) + "\n"
-
-
-def synchronize(prepared_path: Path, verification_path: Path, docs_root: Path) -> tuple[Path, Path, Path]:
-    prepared = load_json(prepared_path)
-    verification = load_json(verification_path)
-
-    if verification.get("success") is not True:
-        raise ValueError("PublicationVerification is not successful")
-    if verification.get("prepared_release_id") != prepared.get("id"):
-        raise ValueError("PublicationVerification does not reference the supplied PreparedRelease")
-    github_release = verification.get("github_release")
-    if not isinstance(github_release, dict) or github_release.get("published") is not True:
-        raise ValueError("GitHub Release is not confirmed as published")
-
-    version = prepared.get("version")
-    changelog = prepared.get("changelog")
-    if not isinstance(version, str) or VERSION_RE.fullmatch(version) is None:
-        raise ValueError("PreparedRelease contains an invalid version")
-    if not isinstance(changelog, dict) or not isinstance(changelog.get("section"), str):
-        raise ValueError("PreparedRelease does not contain a changelog section")
-
-    major = int(version.split(".", 1)[0])
-    history_root = docs_root / "developer_manual" / "release-history"
-    major_root = history_root / str(major)
-    major_root.mkdir(parents=True, exist_ok=True)
-
-    release_path = major_root / release_filename(version)
-    release_path.write_text(markdown_section_to_rst(changelog["section"], version), encoding="utf-8")
-
-    versions = [path.stem for path in major_root.glob("*.rst") if path.name != "index.rst"]
-    major_index = major_root / "index.rst"
-    major_index.write_text(render_major_index(major, versions), encoding="utf-8")
-
-    majors = [int(path.name) for path in history_root.iterdir() if path.is_dir() and path.name.isdigit()]
-    root_index = history_root / "index.rst"
-    root_index.write_text(render_root_index(majors), encoding="utf-8")
-
-    return release_path, major_index, root_index
-
-
-def main() -> int:
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--prepared", required=True, type=Path)
-    parser.add_argument("--verification", required=True, type=Path)
-    parser.add_argument("--docs-root", required=True, type=Path)
-    args = parser.parse_args()
-    release_path, major_index, root_index = synchronize(args.prepared, args.verification, args.docs_root)
-    print(json.dumps({
-        "release_path": str(release_path),
-        "major_index": str(major_index),
-        "root_index": str(root_index),
-    }, separators=(",", ":")))
-    return 0
-
-
-if __name__ == "__main__":
-    raise SystemExit(main())
diff --git a/tests/test_sync_release_history.py b/tests/test_sync_release_history.py
deleted file mode 100644
index 7650cff..0000000
--- a/tests/test_sync_release_history.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-from __future__ import annotations
-
-import importlib.util
-import json
-import tempfile
-import unittest
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[1]
-SCRIPT = ROOT / "scripts" / "sync_release_history.py"
-
-spec = importlib.util.spec_from_file_location("sync_release_history", SCRIPT)
-assert spec is not None and spec.loader is not None
-module = importlib.util.module_from_spec(spec)
-spec.loader.exec_module(module)
-
-
-class SyncReleaseHistoryTest(unittest.TestCase):
-    def test_markdown_release_section_becomes_browsable_rst(self) -> None:
-        section = "## 15.1.0 - 2026-09-21\n\n### Added\n- add feature [#10](https://github.com/LibreSign/libresign/pull/10)\n"
-        rendered = module.markdown_section_to_rst(section, "15.1.0")
-        self.assertIn("15.1.0 - 2026-09-21", rendered)
-        self.assertIn("Added\n-----", rendered)
-        self.assertIn("`#10 `_", rendered)
-        self.assertIn("AGPL-3.0-or-later", rendered)
-
-    def test_synchronization_requires_successful_matching_publication(self) -> None:
-        with tempfile.TemporaryDirectory() as directory:
-            root = Path(directory)
-            prepared = root / "prepared.json"
-            verification = root / "verification.json"
-            prepared.write_text(json.dumps({
-                "id": "prepared-1",
-                "version": "15.1.0",
-                "changelog": {"section": "## 15.1.0 - 2026-09-21\n\n### Fixed\n- fix one\n"},
-            }), encoding="utf-8")
-            verification.write_text(json.dumps({
-                "success": False,
-                "prepared_release_id": "prepared-1",
-                "github_release": {"published": True},
-            }), encoding="utf-8")
-            with self.assertRaisesRegex(ValueError, "not successful"):
-                module.synchronize(prepared, verification, root / "docs")
-
-    def test_synchronization_is_idempotent_and_indexes_major(self) -> None:
-        with tempfile.TemporaryDirectory() as directory:
-            root = Path(directory)
-            prepared = root / "prepared.json"
-            verification = root / "verification.json"
-            prepared.write_text(json.dumps({
-                "id": "prepared-1",
-                "version": "15.1.0",
-                "changelog": {"section": "## 15.1.0 - 2026-09-21\n\n### Changed\n- update translations\n"},
-            }), encoding="utf-8")
-            verification.write_text(json.dumps({
-                "success": True,
-                "prepared_release_id": "prepared-1",
-                "github_release": {"published": True},
-            }), encoding="utf-8")
-            docs = root / "docs"
-            module.synchronize(prepared, verification, docs)
-            first = (docs / "developer_manual/release-history/15/15.1.0.rst").read_text(encoding="utf-8")
-            module.synchronize(prepared, verification, docs)
-            second = (docs / "developer_manual/release-history/15/15.1.0.rst").read_text(encoding="utf-8")
-            self.assertEqual(first, second)
-            self.assertIn("15.1.0", (docs / "developer_manual/release-history/15/index.rst").read_text(encoding="utf-8"))
-            self.assertIn("LibreSign 15 <15/index>", (docs / "developer_manual/release-history/index.rst").read_text(encoding="utf-8"))
-            self.assertIn("AGPL-3.0-or-later", (docs / "developer_manual/release-history/15/index.rst").read_text(encoding="utf-8"))
-
-    def test_versions_sort_semantically(self) -> None:
-        versions = ["15.9.0", "15.10.0", "15.10.0-rc.2", "15.10.0-rc.10", "15.10.0-beta.12", "15.2.4"]
-        rendered = module.render_major_index(15, versions)
-        self.assertLess(rendered.index("15.10.0\n"), rendered.index("15.10.0-rc.10\n"))
-        self.assertLess(rendered.index("15.10.0-rc.10\n"), rendered.index("15.10.0-rc.2\n"))
-        self.assertLess(rendered.index("15.10.0-rc.2\n"), rendered.index("15.10.0-beta.12\n"))
-        self.assertLess(rendered.index("15.10.0-beta.12\n"), rendered.index("15.9.0\n"))
-
-
-if __name__ == "__main__":
-    unittest.main()