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 with:
app-id: 3060111
private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }}
owner: hotdata-dev
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
token: ${{ steps.app-token.outputs.token }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 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
- name: Clean generated source
run: rm -rf src/apis src/models docs
- 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: |
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 \
--http-user-agent "hotdata-rust/${PACKAGE_VERSION}" \
--skip-validate-spec
- name: Clean up fetched spec
run: rm -f openapi.yaml
- 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."
- name: Verify generated client compiles
run: |
cargo check --all-features
cargo test --all-features --no-run
- 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 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.' }}"