From 2e2eca7f2a96ca78bf759152327a89b2f0a8d263 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Jul 2026 14:01:13 -0400 Subject: [PATCH] auto deploys --- .github/workflows/sdk_generation.yaml | 11 ++- .github/workflows/sdk_publish.yaml | 67 +++++++++++++ .speakeasy/gen.yaml | 2 +- .speakeasy/workflow.yaml | 2 +- scripts/render_mintlify_releases.py | 125 +++++++++++++++++++++++++ tests/test_render_mintlify_releases.py | 51 ++++++++++ 6 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 scripts/render_mintlify_releases.py create mode 100644 tests/test_render_mintlify_releases.py diff --git a/.github/workflows/sdk_generation.yaml b/.github/workflows/sdk_generation.yaml index b33b584..c7b2d83 100644 --- a/.github/workflows/sdk_generation.yaml +++ b/.github/workflows/sdk_generation.yaml @@ -1,4 +1,5 @@ name: Generate +run-name: ${{ github.event_name == 'repository_dispatch' && format('Generate Python SDK for {0}', github.event.client_payload.deployment_tag) || 'Generate Python SDK' }} permissions: checks: write contents: write @@ -15,17 +16,23 @@ permissions: set_version: description: optionally set a specific SDK version type: string + repository_dispatch: + types: + - production-openapi-published pull_request: types: - labeled - unlabeled +concurrency: + group: sdk-generation + cancel-in-progress: false jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: - force: ${{ github.event.inputs.force }} + force: ${{ github.event_name == 'repository_dispatch' || inputs.force }} mode: pr - set_version: ${{ github.event.inputs.set_version }} + set_version: ${{ inputs.set_version }} secrets: github_access_token: ${{ secrets.GH_PAT }} pypi_token: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/sdk_publish.yaml b/.github/workflows/sdk_publish.yaml index 7b5d64d..75654cf 100644 --- a/.github/workflows/sdk_publish.yaml +++ b/.github/workflows/sdk_publish.yaml @@ -21,3 +21,70 @@ jobs: github_access_token: ${{ secrets.GH_PAT }} pypi_token: ${{ secrets.PYPI_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} + + sync-python-sdk-docs: + needs: [publish] + runs-on: ubuntu-latest + steps: + - name: Checkout Python SDK + uses: actions/checkout@v4 + with: + path: sdk + + - name: Checkout TextQL docs + uses: actions/checkout@v4 + with: + repository: TextQLLabs/demo2 + token: ${{ secrets.GH_PAT }} + path: demo2 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Snapshot combined OpenAPI with SDK samples + run: | + curl --fail --silent --show-error --location \ + --header 'Cache-Control: no-cache' \ + --retry 10 --retry-all-errors --retry-delay 10 \ + https://spec.speakeasy.com/textql/home/textql-api-with-code-samples \ + --output demo2/docs/public/api-reference/speakeasy/textql-api-with-code-samples.yaml + + - name: Render Mintlify API reference + run: | + python3 -m pip install --disable-pip-version-check PyYAML==6.0.3 + python3 demo2/scripts/render_sdk_api_reference.py + + - name: Render Mintlify release log + run: | + python3 sdk/scripts/render_mintlify_releases.py \ + sdk/RELEASES.md \ + demo2/docs/public/api-reference/sdk/changelog.mdx + + - name: Create or update docs pull request + id: docs-pr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GH_PAT }} + path: demo2 + branch: automation/python-sdk-docs + delete-branch: true + commit-message: "docs: sync Python SDK reference" + title: "docs: sync Python SDK reference" + body: | + Automated after a successful Python SDK publish. + + - Snapshots the combined Speakeasy OpenAPI document and SDK code samples. + - Regenerates stable Mintlify API-reference wrappers. + - Updates the Python SDK release log from `TextQLLabs/textql-python-v3/RELEASES.md`. + + - name: Auto-merge docs pull request after checks pass + if: steps.docs-pr.outputs.pull-request-number != '' + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + DOCS_PR_NUMBER: ${{ steps.docs-pr.outputs.pull-request-number }} + run: | + gh pr merge "$DOCS_PR_NUMBER" \ + --repo TextQLLabs/demo2 \ + --auto \ + --squash diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index d82c55a..b0f5734 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -27,7 +27,7 @@ generation: schemas: allOfMergeStrategy: shallowMerge requestBodyFieldName: body - versioningStrategy: manual + versioningStrategy: automatic persistentEdits: {} tests: generateTests: false diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index 8f66681..5baac97 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -21,4 +21,4 @@ targets: location: registry.speakeasyapi.dev/textql/home/textql-api-python-code-samples labelOverride: fixedValue: Python (SDK) - blocking: false + blocking: true diff --git a/scripts/render_mintlify_releases.py b/scripts/render_mintlify_releases.py new file mode 100644 index 0000000..ca99a99 --- /dev/null +++ b/scripts/render_mintlify_releases.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Render Speakeasy's RELEASES.md as a Mintlify Python SDK release log.""" + +from __future__ import annotations + +import argparse +import re +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + + +FRONTMATTER = """--- +title: "Python SDK release log" +description: "Release history for the TextQL Python SDK" +icon: "python" +--- + +Install the latest release from [PyPI](https://pypi.org/project/textql-sdk/): + +```bash +pip install textql-sdk +``` +""" + +SECTION = re.compile( + r"^## (?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*$", + re.MULTILINE, +) +PYPI_LINK = re.compile( + r"\[PyPI v(?P[^\]]+)\](?:\((?Phttps?://[^)]+)\)|\s+(?Phttps?://\S+))" +) +SPEAKEASY = re.compile( + r"^- Speakeasy CLI (?P\S+)(?: \((?P[^)]+)\))?(?: (?Phttps?://\S+))?\s*$", + re.MULTILINE, +) + + +@dataclass(frozen=True) +class Release: + published_at: datetime + version: str + pypi_url: str + speakeasy_cli: str | None = None + generator_version: str | None = None + speakeasy_url: str | None = None + + +def parse_releases(markdown: str) -> list[Release]: + """Parse the release records generated by Speakeasy.""" + matches = list(SECTION.finditer(markdown)) + if not matches: + raise ValueError("RELEASES.md does not contain any timestamped releases") + + releases: list[Release] = [] + for index, match in enumerate(matches): + end = matches[index + 1].start() if index + 1 < len(matches) else len(markdown) + body = markdown[match.end() : end] + pypi = PYPI_LINK.search(body) + if pypi is None: + raise ValueError(f"release {match.group('timestamp')} has no PyPI link") + + speakeasy = SPEAKEASY.search(body) + releases.append( + Release( + published_at=datetime.strptime( + match.group("timestamp"), "%Y-%m-%d %H:%M:%S" + ), + version=pypi.group("version"), + pypi_url=pypi.group("link_url") or pypi.group("plain_url"), + speakeasy_cli=speakeasy.group("cli") if speakeasy else None, + generator_version=speakeasy.group("generator") if speakeasy else None, + speakeasy_url=speakeasy.group("url") if speakeasy else None, + ) + ) + return releases + + +def render_release(release: Release) -> str: + date = ( + f"{release.published_at.strftime('%B')} " + f"{release.published_at.day}, {release.published_at.year}" + ) + lines = [ + f'', + "", + f"[View `textql-sdk` v{release.version} on PyPI]({release.pypi_url}).", + "", + "```bash", + f"pip install textql-sdk=={release.version}", + "```", + ] + if release.speakeasy_cli: + label = f"Speakeasy CLI {release.speakeasy_cli}" + if release.generator_version: + label += f" (generator {release.generator_version})" + if release.speakeasy_url: + lines.extend(["", f"Generated with [{label}]({release.speakeasy_url})."]) + else: + lines.extend(["", f"Generated with {label}."]) + lines.extend(["", ""]) + return "\n".join(lines) + + +def render(markdown: str) -> str: + releases = parse_releases(markdown) + return FRONTMATTER.rstrip() + "\n\n" + "\n\n".join( + render_release(release) for release in releases + ) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("source", type=Path, help="Speakeasy RELEASES.md") + parser.add_argument("destination", type=Path, help="Mintlify MDX output") + args = parser.parse_args() + + result = render(args.source.read_text()) + args.destination.parent.mkdir(parents=True, exist_ok=True) + args.destination.write_text(result) + print(f"Rendered {len(parse_releases(args.source.read_text()))} Python SDK release(s)") + + +if __name__ == "__main__": + main() diff --git a/tests/test_render_mintlify_releases.py b/tests/test_render_mintlify_releases.py new file mode 100644 index 0000000..4abf3ad --- /dev/null +++ b/tests/test_render_mintlify_releases.py @@ -0,0 +1,51 @@ +import unittest + +from scripts.render_mintlify_releases import parse_releases, render + + +RELEASE = """ +## 2026-07-17 21:45:13 +### Changes +Based on: +- OpenAPI Doc +- Speakeasy CLI 1.790.2 (2.918.3) https://github.com/speakeasy-api/speakeasy +### Generated +- [python v1.0.0] . +### Releases +- [PyPI v1.0.0] https://pypi.org/project/textql-sdk/1.0.0 - . +""" + + +class RenderMintlifyReleasesTest(unittest.TestCase): + def test_parses_speakeasy_release_format(self) -> None: + release = parse_releases(RELEASE)[0] + self.assertEqual(release.version, "1.0.0") + self.assertEqual( + release.pypi_url, "https://pypi.org/project/textql-sdk/1.0.0" + ) + self.assertEqual(release.speakeasy_cli, "1.790.2") + self.assertEqual(release.generator_version, "2.918.3") + + def test_accepts_standard_markdown_pypi_link(self) -> None: + markdown = RELEASE.replace( + "[PyPI v1.0.0] https://pypi.org/project/textql-sdk/1.0.0 - .", + "[PyPI v1.0.0](https://pypi.org/project/textql-sdk/1.0.0)", + ) + self.assertEqual(parse_releases(markdown)[0].version, "1.0.0") + + def test_renders_mintlify_updates(self) -> None: + output = render(RELEASE) + self.assertIn('title: "Python SDK release log"', output) + self.assertIn( + '', output + ) + self.assertIn("pip install textql-sdk==1.0.0", output) + self.assertIn("[Speakeasy CLI 1.790.2 (generator 2.918.3)]", output) + + def test_rejects_release_without_pypi_link(self) -> None: + with self.assertRaisesRegex(ValueError, "no PyPI link"): + parse_releases(RELEASE.replace("PyPI", "Package")) + + +if __name__ == "__main__": + unittest.main()