-
Notifications
You must be signed in to change notification settings - Fork 27
303 lines (280 loc) · 13.8 KB
/
Copy pathgenerate-command.yml
File metadata and controls
303 lines (280 loc) · 13.8 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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# Speakeasy SDK Generation Workflow
#
# This workflow regenerates the Python SDK code using Speakeasy.
# It can create a new PR, update an existing PR branch, or run in dry-run mode for validation.
#
# Triggers:
# - On push to main: Auto-generates after every merge to ensure SDK stays up-to-date (auto-merge enabled)
# - Daily schedule (5 AM & 5 PM America/Los_Angeles): Catches upstream API spec changes (auto-merge enabled)
# - Manual workflow_dispatch: For on-demand generation
# - Slash command (/generate): Regenerates and pushes results back to the PR branch
# - workflow_call: For validation from other workflows (e.g., PR checks)
#
# Generation Process:
# 1. Install Speakeasy CLI from pinned Docker image
# 2. Run Speakeasy to generate the Python SDK code
# 3. Run post-generation patches (currently no-op)
# 4. (If PR context) Commit and push regenerated code back to the PR branch
# 5. (If no PR context and not dry_run) Create a new PR with the regenerated code
# 6. (If dry_run) Verify the generated code is valid
#
# How to use:
# - From a PR: Comment `/generate` to regenerate and push to the PR branch
# - From Actions: Go to Actions > Generate > Run workflow (creates a new PR)
# - Optionally check "Dry run" to validate generation without committing
name: Generate SDK
"on":
push:
branches:
- main
schedule:
- cron: '0 5 * * *'
timezone: America/Los_Angeles
- cron: '0 17 * * *'
timezone: America/Los_Angeles
workflow_dispatch:
inputs:
dry_run:
description: Validate generation without creating a PR
type: boolean
default: false
pr:
description: 'PR number (if set, pushes results to the PR branch instead of creating a new PR)'
type: string
required: false
comment-id:
description: 'Comment ID (for slash command triggers)'
type: string
required: false
workflow_call:
inputs:
dry_run:
description: Validate generation without creating a PR
type: boolean
default: false
outputs:
has_changes:
description: Whether the generation produced changes vs committed code
value: ${{ jobs.generate.outputs.has_changes }}
drift_summary:
description: Git diff stat summary when drift is detected
value: ${{ jobs.generate.outputs.drift_summary }}
concurrency:
group: ${{ (github.event_name == 'push' || github.event_name == 'schedule') && 'generate-new-pr' || format('generate-{0}', github.run_id) }}
cancel-in-progress: true
jobs:
check-paths:
name: Check Generation Paths
if: ${{ inputs.dry_run }}
runs-on: ubuntu-latest
outputs:
should_run: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.generation == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Filter changed paths
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
generation:
- '.speakeasy/**'
- '.genignore'
- '.github/speakeasy/**'
- 'gen.yaml'
- 'overlays/**'
- 'README.md'
- 'scripts/**'
- 'poe_tasks.toml'
- 'src/**'
generate:
name: Generate SDK
needs: [check-paths]
if: ${{ always() && (!inputs.dry_run || needs.check-paths.outputs.should_run == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
has_changes: ${{ steps.changes.outputs.has_changes }}
drift_summary: ${{ steps.changes.outputs.drift_summary }}
permissions:
contents: write
pull-requests: write
steps:
- name: Authenticate as GitHub App
uses: actions/create-github-app-token@v3
id: app-token
continue-on-error: ${{ github.actor == 'dependabot[bot]' }}
with:
app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }}
private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }}
- name: Warn on GitHub App auth fallback
if: steps.app-token.outcome == 'failure'
run: |
echo "::warning::GitHub App authentication failed (secrets may not be available in this context). Falling back to GITHUB_TOKEN."
- name: Post or append starting comment
if: ${{ !inputs.dry_run && github.event.inputs.pr != '' }}
id: start-comment
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.inputs.pr }}
comment-id: ${{ github.event.inputs.comment-id || '' }}
body: |
> **Generate SDK Job Info**
>
> Running Speakeasy SDK generation.
> Job started... [Check job output.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
- name: Resolve PR head branch
if: ${{ !inputs.dry_run && github.event.inputs.pr != '' }}
id: pr-branch
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr }}
run: |
PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')
IS_FORK=$(echo "$PR_JSON" | jq -r '.head.repo.fork')
if [ "$IS_FORK" = "true" ]; then
echo "::error::Cannot run /generate on fork PRs. Please regenerate locally."
exit 1
fi
echo "head_ref=${HEAD_REF}" >> $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ steps.pr-branch.outputs.head_ref || '' }}
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Get next version from release drafter
id: get-version
uses: aaronsteers/semantic-pr-release-drafter@v2.0.1
with:
dry-run: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install Speakeasy CLI
run: |
SPEAKEASY_IMAGE=$(yq '.services.speakeasy.image' .github/speakeasy/dummy-compose.yml)
echo "Pinned Speakeasy image: $SPEAKEASY_IMAGE"
docker pull "$SPEAKEASY_IMAGE"
CONTAINER_ID=$(docker create "$SPEAKEASY_IMAGE")
sudo docker cp "$CONTAINER_ID:/usr/local/bin/speakeasy" /usr/local/bin/speakeasy
docker rm "$CONTAINER_ID" >/dev/null
speakeasy --version
- name: Resolve SDK version
id: resolve-version
env:
DRAFTER_VERSION: ${{ steps.get-version.outputs.resolved-version }}
run: |
GENYAML_VERSION=$(yq '.python.version' gen.yaml)
echo "Release drafter version: ${DRAFTER_VERSION:-<empty>}"
echo "gen.yaml version: ${GENYAML_VERSION:-<empty>}"
# Use gen.yaml version if it is a higher major than the drafter
# (handles initial major-version bumps before the first release).
# Otherwise, prefer the release drafter's resolved version.
DRAFTER_MAJOR=${DRAFTER_VERSION%%.*}
GENYAML_MAJOR=${GENYAML_VERSION%%.*}
if [ -n "$GENYAML_VERSION" ] && [ "${GENYAML_MAJOR:-0}" -gt "${DRAFTER_MAJOR:-0}" ]; then
echo "version=${GENYAML_VERSION}" | tee -a $GITHUB_OUTPUT
echo "Using gen.yaml version (higher major: ${GENYAML_MAJOR} > ${DRAFTER_MAJOR})"
elif [ -n "$DRAFTER_VERSION" ]; then
echo "version=${DRAFTER_VERSION}" | tee -a $GITHUB_OUTPUT
echo "Using release drafter version"
elif [ -n "$GENYAML_VERSION" ]; then
echo "version=${GENYAML_VERSION}" | tee -a $GITHUB_OUTPUT
echo "Falling back to gen.yaml version"
else
echo "::error::No version could be resolved from release drafter or gen.yaml."
exit 1
fi
- name: Generate SDK
env:
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
VERSION: ${{ steps.resolve-version.outputs.version }}
run: |
echo "Generating with version: $VERSION"
uv run poe generate-full
- name: Generation Summary
run: |
echo "=== Generation Summary ==="
echo "Source files: $(find src/ -name '*.py' 2>/dev/null | wc -l)"
echo "Model files: $(find src/ -path '*/models/*' -name '*.py' 2>/dev/null | wc -l)"
if [ -f "pyproject.toml" ]; then
echo "Package version: $(grep 'version' pyproject.toml | head -1)"
fi
- name: Check for changes
id: changes
run: |
# Restore non-deterministic Speakeasy lock files to HEAD
# to ignore digest changes that cause infinite generate→merge loops.
git checkout HEAD -- .speakeasy/workflow.lock 2>/dev/null || true
git checkout HEAD -- .speakeasy/gen.lock 2>/dev/null || true
if [ -n "$(git status --porcelain)" ]; then
echo "has_changes=true" | tee -a $GITHUB_OUTPUT
echo "=== Changed files ==="
git status --porcelain
echo
echo "=== Diff stat ==="
SUMMARY=$(git diff --stat)
echo "$SUMMARY"
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
{
echo "drift_summary<<$EOF"
echo "$SUMMARY"
echo "$EOF"
} | tee -a "$GITHUB_OUTPUT"
else
echo "has_changes=false" | tee -a $GITHUB_OUTPUT
fi
# --- PR branch mode: commit and push to the existing PR branch ---
- name: Push regenerated code to PR branch
if: ${{ !inputs.dry_run && github.event.inputs.pr != '' && steps.changes.outputs.has_changes == 'true' }}
run: |
git config user.name "octavia-bot[bot]"
git config user.email "octavia-bot[bot]@users.noreply.github.com"
git add -A
git commit -m "chore: regenerate SDK with Speakeasy"
git push
# --- New PR mode: create a PR to main ---
- name: Create Pull Request
if: ${{ !inputs.dry_run && steps.changes.outputs.has_changes == 'true' && github.event.inputs.pr == '' }}
id: create-pr
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
commit-message: "chore: regenerate SDK with Speakeasy"
title: "chore: regenerate SDK with Speakeasy"
body: |
This PR was automatically generated by the Speakeasy SDK generation workflow.
Please review the changes and merge if they look correct.
branch: speakeasy-sdk-regen
base: main
delete-branch: true
- name: Enable auto-merge (new PR only)
if: |
(github.event_name == 'push'
|| github.event_name == 'schedule'
) && steps.create-pr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
run: gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --squash
- name: Append success comment
if: ${{ success() && !inputs.dry_run && github.event.inputs.pr != '' }}
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
comment-id: ${{ steps.start-comment.outputs.comment-id }}
reactions: hooray
body: |
> SDK generation completed successfully.
- name: Append failure comment
if: ${{ failure() && !inputs.dry_run && github.event.inputs.pr != '' }}
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
comment-id: ${{ steps.start-comment.outputs.comment-id }}
reactions: confused
body: |
> SDK generation failed. Check the [job output](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.