-
Notifications
You must be signed in to change notification settings - Fork 0
289 lines (262 loc) · 13.3 KB
/
Copy pathregenerate.yml
File metadata and controls
289 lines (262 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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:
client-id: Iv23liKBX2RYMoZIYuKa
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
components: rustfmt
- 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
# Read (do NOT bump) the current crate version. A regen is just a set of
# changes; which release ships them — and the bump kind — is decided later
# by the release tooling, not minted per regen. The version is still read
# here because the generator bakes it into the default user-agent and
# packageVersion below, so generated output reflects the committed crate
# version (Cargo.toml is ignore-protected, so the generator never edits it).
- name: Read package version
id: pkg
run: |
version=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name=="hotdata") | .version')
echo "version=$version" >> "$GITHUB_OUTPUT"
# Seed the regen notes under [Unreleased] so they accumulate there until a
# release rolls them into a version. check-release.py is a no-op when the
# version is unchanged, so a non-bumping regen PR still passes; the regen
# still touches CHANGELOG.md, so check-release.yml runs as before.
- name: Seed changelog entry under [Unreleased]
env:
TITLE: ${{ inputs.title }}
run: |
python3 - <<'PY'
import os, pathlib, re
title = (os.environ.get("TITLE") or "").strip() \
or "Regenerate the client from the updated Hotdata OpenAPI spec"
bullet = f"- {title}"
path = pathlib.Path("CHANGELOG.md")
text = path.read_text()
heading = re.search(r"^## \[Unreleased\][^\n]*\n", text, re.M)
if not heading:
raise SystemExit("CHANGELOG.md has no '## [Unreleased]' section to anchor the new entry")
# Scope edits to the [Unreleased] body (up to the next '## [' or EOF).
start = heading.end()
nxt = re.search(r"^## \[", text[start:], re.M)
end = start + nxt.start() if nxt else len(text)
body = text[start:end]
if any(line.strip() == bullet for line in body.splitlines()):
print("CHANGELOG [Unreleased] already lists this entry; leaving it untouched.")
raise SystemExit(0)
changed = re.search(r"^### Changed[ \t]*\n", body, re.M)
if changed:
# Prepend the bullet to the existing ### Changed list.
i = changed.end()
while i < len(body) and body[i] == "\n":
i += 1
body = body[:i] + bullet + "\n" + body[i:]
else:
# No ### Changed yet: open one right under the heading.
body = "\n### Changed\n\n" + bullet + "\n\n" + body.lstrip("\n")
path.write_text(text[:start] + body + text[end:])
print("Added regen entry under CHANGELOG [Unreleased].")
PY
- 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
# The 7.22.0 generator emits compact, non-rustfmt output, which buries real
# spec changes under formatting churn in every regen diff. Format only the
# generated subtrees so the diff shows semantic changes; the hand-written
# ergonomic layer is left to its own formatting (and is ignore-protected
# from the generator anyway).
- name: Format generated code
run: |
# Derive the edition from Cargo.toml so it can't drift from the crate
# if the edition is ever bumped (same `cargo metadata` source as the
# version read above).
edition=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name=="hotdata") | .edition')
find src/apis src/models -name '*.rs' -print0 \
| xargs -0 rustfmt --edition "$edition"
- 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."
# NOTE: regeneration deliberately does NOT compile the crate. A compile
# failure here used to abort the job before "Create PR", so a regen that
# produced valid-but-not-yet-wired output (e.g. a spec change that orphans
# part of the hand-written ergonomic layer) failed silently with no PR to
# act on. The PR is the artifact we want: integration-tests.yml runs
# `cargo test --all-features` on every PR to main, so any breakage shows
# up loudly as red CI on the filed PR instead of a buried workflow 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.' }}"