Regenerate Client #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Regenerate Client | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| title: | |
| description: PR title, auto-generated from the spec diff in www. | |
| required: false | |
| type: string | |
| summary: | |
| description: PR body summarizing the spec changes. | |
| required: false | |
| type: string | |
| jobs: | |
| regenerate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: 3060111 | |
| private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }} | |
| owner: hotdata-dev | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| toolchain: stable | |
| - name: Fetch merged OpenAPI spec | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| curl -sS -f -L \ | |
| -H "Accept: application/vnd.github.v3.raw" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| https://api.github.com/repos/hotdata-dev/www.hotdata.dev/contents/api/openapi.yaml \ | |
| -o openapi.yaml | |
| # OpenAPI 3.1 lets a `$ref` carry sibling keywords (e.g. a per-branch | |
| # `description` on a `oneOf` member). That's valid, and the Python/TS | |
| # generators consume it fine, but the Rust backend NPEs on it | |
| # (AbstractRustCodegen.toModelName — reproduced through generator 7.22.0, | |
| # no upstream fix yet). Flatten `$ref`-with-siblings to pure refs in our | |
| # throwaway generation input only; the canonical www spec and the other | |
| # SDKs are untouched. See scripts/normalize-openapi.py. | |
| - name: Normalize spec for the Rust generator | |
| run: | | |
| python3 -c "import yaml" 2>/dev/null || pip3 install --quiet pyyaml | |
| python3 scripts/normalize-openapi.py openapi.yaml | |
| # Targeted clean: delete ONLY generator-owned subtrees. The hand-written | |
| # ergonomic layer (src/lib.rs, src/auth.rs, src/arrow.rs, src/client.rs) | |
| # is preserved here and additionally protected by .openapi-generator-ignore | |
| # so the generator re-emits src/apis and src/models in place without | |
| # clobbering the regen-immune modules or Cargo.toml. | |
| - name: Clean generated source | |
| run: rm -rf src/apis src/models docs | |
| # Cargo.toml is ignore-protected, so the generator no longer rewrites the | |
| # version. We own the patch bump here via cargo-edit and feed the result | |
| # to the generator as packageVersion + user-agent for consistency. | |
| - name: Bump patch version | |
| id: pkg | |
| run: | | |
| cargo install cargo-edit --locked >/dev/null 2>&1 || cargo install cargo-edit | |
| cargo set-version --bump patch | |
| version=$(cargo metadata --no-deps --format-version 1 \ | |
| | jq -r '.packages[] | select(.name=="hotdata") | .version') | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Generate client | |
| env: | |
| PACKAGE_VERSION: ${{ steps.pkg.outputs.version }} | |
| run: | | |
| # useChrono=false: 7.22.0 flipped the Rust generator's useChrono | |
| # default to true, which emits chrono::DateTime for date-time fields. | |
| # The crate has no chrono dependency and the ergonomic layer + tests | |
| # treat dates as String (as 7.20.0 generated them), so keep String. | |
| npx @openapitools/openapi-generator-cli generate \ | |
| -i openapi.yaml \ | |
| -g rust \ | |
| -o . \ | |
| -t .openapi-generator-templates \ | |
| --additional-properties=packageName=hotdata,packageVersion=$PACKAGE_VERSION,library=reqwest,supportAsync=true,useChrono=false \ | |
| --http-user-agent "hotdata-rust/${PACKAGE_VERSION}" \ | |
| --skip-validate-spec | |
| - name: Clean up fetched spec | |
| run: rm -f openapi.yaml | |
| # Regen-safety guard (the Rust analog of sdk-python's AST check). Fails the | |
| # build loudly if the generator clobbered the hand-written ergonomic layer | |
| # or if the JWT/bearer template hooks were dropped or renamed by a future | |
| # generator version. Runs BEFORE cargo check so the failure is precise. | |
| - name: Verify ergonomic layer survived regen | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| # 1. Hand-written, regen-immune modules must exist. | |
| for f in src/lib.rs src/auth.rs src/arrow.rs src/client.rs src/resources.rs src/field.rs src/status.rs src/http_log.rs; do | |
| if [ ! -f "$f" ]; then | |
| echo "::error::$f is missing (regen clobbered the ergonomic layer)" | |
| fail=1 | |
| fi | |
| done | |
| # 2. lib.rs must still wire the hand-written modules. | |
| for decl in 'pub mod auth;' 'pub mod client;' 'pub mod arrow;' 'pub mod resources;' 'pub mod field;' 'pub mod status;' 'pub mod http_log;'; do | |
| if ! grep -q "$decl" src/lib.rs; then | |
| echo "::error::src/lib.rs no longer declares '$decl'" | |
| fail=1 | |
| fi | |
| done | |
| # 3. lib.rs must still re-export the ergonomic surface. | |
| if ! grep -Eq 'pub use (crate::)?auth::' src/lib.rs; then | |
| echo "::error::src/lib.rs no longer re-exports the auth surface" | |
| fail=1 | |
| fi | |
| if ! grep -Eq 'pub use (crate::)?client::' src/lib.rs; then | |
| echo "::error::src/lib.rs no longer re-exports the client surface" | |
| fail=1 | |
| fi | |
| # 4. The JWT/bearer template hooks must have survived the generator. | |
| # configuration.mustache emits resolve_bearer_token on Configuration; | |
| # api.mustache calls it at every bearer-auth site. | |
| if ! grep -q 'resolve_bearer_token' src/apis/configuration.rs; then | |
| echo "::error::resolve_bearer_token missing from generated configuration.rs (configuration.mustache drift)" | |
| fail=1 | |
| fi | |
| if ! grep -rq 'resolve_bearer_token().await' src/apis/; then | |
| echo "::error::resolve_bearer_token().await missing from generated apis (api.mustache bearer hook drift)" | |
| fail=1 | |
| fi | |
| # 5. The request/response debug-logging hooks must have survived the | |
| # generator. api.mustache emits crate::http_log::log_request + | |
| # log_response_status/_body at every op (issue #135). | |
| if ! grep -rq 'crate::http_log::log_request' src/apis/; then | |
| echo "::error::http_log::log_request missing from generated apis (api.mustache debug-logging hook drift)" | |
| fail=1 | |
| fi | |
| if ! grep -rq 'crate::http_log::log_response_status' src/apis/; then | |
| echo "::error::http_log::log_response_status missing from generated apis (api.mustache debug-logging hook drift)" | |
| fail=1 | |
| fi | |
| if [ "$fail" -ne 0 ]; then | |
| echo "::error::Regen-safety check failed: the ergonomic layer or auth/logging hooks did not survive regeneration." | |
| exit 1 | |
| fi | |
| echo "Ergonomic layer survived regeneration: hand-written modules, lib.rs wiring, and JWT/bearer + debug-logging hooks all intact." | |
| # cargo check is the template-drift tripwire. --all-features compiles the | |
| # optional arrow surface too. The --no-run test build catches breakage in | |
| # the integration tests that target the generated apis. | |
| - name: Verify generated client compiles | |
| run: | | |
| cargo check --all-features | |
| cargo test --all-features --no-run | |
| # Soft parity warning: surface scenarios that still lack a tests/<name>.rs. | |
| # Non-fatal during the per-scenario rollout (rust is exempt until its tests | |
| # land); the enforcing gate lives in integration-tests.yml. | |
| - name: Check integration test scenario parity | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| curl -sS -f -L \ | |
| -H "Accept: application/vnd.github.v3.raw" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| https://api.github.com/repos/hotdata-dev/www.hotdata.dev/contents/api/test-scenarios.yaml \ | |
| -o test-scenarios.yaml | |
| python3 - <<'PY' | |
| import sys, pathlib, re | |
| text = pathlib.Path("test-scenarios.yaml").read_text() | |
| # Minimal stdlib parse: walk "- name:" blocks, capture optional_for. | |
| missing = [] | |
| total = 0 | |
| name = None | |
| optional = [] | |
| def flush(name, optional): | |
| if name is None: | |
| return | |
| if "rust" in optional: | |
| return | |
| expected = pathlib.Path("tests") / f"{name}.rs" | |
| if not expected.exists(): | |
| missing.append(str(expected)) | |
| for line in text.splitlines(): | |
| m = re.match(r"\s*-\s+name:\s*(\S+)", line) | |
| if m: | |
| flush(name, optional) | |
| total += 1 | |
| name = m.group(1).strip().strip('"\'') | |
| optional = [] | |
| continue | |
| mo = re.match(r"\s*optional_for:\s*\[(.*)\]", line) | |
| if mo: | |
| optional = [x.strip().strip('"\'') for x in mo.group(1).split(",") if x.strip()] | |
| flush(name, optional) | |
| if missing: | |
| print(f"::warning::sdk-rust is missing tests for {len(missing)} scenarios after regen:") | |
| for m in missing: | |
| print(f" - {m}") | |
| else: | |
| print(f"All {total} scenarios have corresponding test files (or are exempt for rust).") | |
| PY | |
| rm -f test-scenarios.yaml | |
| - name: Create PR | |
| id: cpr | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| title: "${{ inputs.title || 'chore: regenerate client from updated OpenAPI spec' }}" | |
| branch: openapi-update-${{ github.run_id }} | |
| commit-message: "${{ inputs.title || 'chore: regenerate client from OpenAPI spec' }}" | |
| body: "${{ inputs.summary || 'Auto-generated from updated HotData OpenAPI spec.' }}" |