1- # Stub: Speakeasy SDK Generation Workflow
1+ # Speakeasy SDK Generation Workflow
22#
3- # This is a minimal placeholder to register the workflow_dispatch trigger on main.
4- # The full implementation will arrive in a follow-up PR.
5- # Without this stub on main, workflow_dispatch and /generate slash commands
6- # cannot target feature branches.
3+ # This workflow regenerates the Python SDK code using Speakeasy.
4+ # It can create a new PR, update an existing PR branch, or run in dry-run mode for validation.
5+ #
6+ # Triggers:
7+ # - On push to main: Auto-generates after every merge to ensure SDK stays up-to-date (auto-merge enabled)
8+ # - Daily schedule (6 AM UTC): Catches upstream API spec changes (auto-merge enabled)
9+ # - Manual workflow_dispatch: For on-demand generation
10+ # - Slash command (/generate): Regenerates and pushes results back to the PR branch
11+ # - workflow_call: For validation from other workflows (e.g., PR checks)
12+ #
13+ # Generation Process:
14+ # 1. Install Speakeasy CLI from pinned Docker image
15+ # 2. Run Speakeasy to generate the Python SDK code
16+ # 3. Run post-generation patches (currently no-op)
17+ # 4. (If PR context) Commit and push regenerated code back to the PR branch
18+ # 5. (If no PR context and not dry_run) Create a new PR with the regenerated code
19+ # 6. (If dry_run) Verify the generated code is valid
20+ #
21+ # How to use:
22+ # - From a PR: Comment `/generate` to regenerate and push to the PR branch
23+ # - From Actions: Go to Actions > Generate > Run workflow (creates a new PR)
24+ # - Optionally check "Dry run" to validate generation without committing
725
826name : Generate SDK
927
1028" on " :
29+ push :
30+ branches :
31+ - main
32+ schedule :
33+ - cron : ' 0 6 * * *'
1134 workflow_dispatch :
1235 inputs :
1336 dry_run :
@@ -29,10 +52,231 @@ name: Generate SDK
2952 type : boolean
3053 default : false
3154
55+ concurrency :
56+ group : ${{ (github.event_name == 'push' || github.event_name == 'schedule') && 'generate-new-pr' || format('generate-{0}', github.run_id) }}
57+ cancel-in-progress : true
58+
3259jobs :
33- stub :
34- name : Stub (placeholder)
60+ generate :
61+ name : Generate SDK
3562 runs-on : ubuntu-latest
63+ timeout-minutes : 30
64+ permissions :
65+ contents : write
66+ pull-requests : write
3667 steps :
37- - name : Placeholder
38- run : echo "This is a stub workflow. The full implementation will arrive in a follow-up PR."
68+ - name : Authenticate as GitHub App
69+ if : ${{ !inputs.dry_run && github.event.inputs.pr != '' }}
70+ uses : actions/create-github-app-token@v3
71+ id : get-app-token
72+ continue-on-error : true
73+ with :
74+ app-id : ${{ secrets.OCTAVIA_BOT_APP_ID }}
75+ private-key : ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }}
76+
77+ - name : Set working token
78+ id : token
79+ run : |
80+ if [ -n "${{ steps.get-app-token.outputs.token }}" ]; then
81+ echo "token=${{ steps.get-app-token.outputs.token }}" | tee -a $GITHUB_OUTPUT
82+ else
83+ echo "token=${{ github.token }}" | tee -a $GITHUB_OUTPUT
84+ fi
85+
86+ - name : Post or append starting comment
87+ if : ${{ !inputs.dry_run && github.event.inputs.pr != '' }}
88+ id : start-comment
89+ uses : peter-evans/create-or-update-comment@v5
90+ with :
91+ token : ${{ steps.token.outputs.token }}
92+ issue-number : ${{ github.event.inputs.pr }}
93+ comment-id : ${{ github.event.inputs.comment-id || '' }}
94+ body : |
95+ > **Generate SDK Job Info**
96+ >
97+ > Running Speakeasy SDK generation.
98+
99+ > Job started... [Check job output.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
100+
101+ - name : Resolve PR head branch
102+ if : ${{ !inputs.dry_run && github.event.inputs.pr != '' }}
103+ id : pr-branch
104+ env :
105+ GH_TOKEN : ${{ steps.token.outputs.token }}
106+ PR_NUMBER : ${{ github.event.inputs.pr }}
107+ run : |
108+ PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
109+ HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')
110+ IS_FORK=$(echo "$PR_JSON" | jq -r '.head.repo.fork')
111+ if [ "$IS_FORK" = "true" ]; then
112+ echo "::error::Cannot run /generate on fork PRs. Please regenerate locally."
113+ exit 1
114+ fi
115+ echo "head_ref=${HEAD_REF}" >> $GITHUB_OUTPUT
116+
117+ - name : Checkout repository
118+ uses : actions/checkout@v4
119+ with :
120+ fetch-depth : 0
121+ ref : ${{ steps.pr-branch.outputs.head_ref || '' }}
122+ token : ${{ steps.token.outputs.token || github.token }}
123+
124+ - name : Install uv
125+ uses : astral-sh/setup-uv@v5
126+
127+ - name : Set up Python
128+ uses : actions/setup-python@v5
129+ with :
130+ python-version : ' 3.12'
131+
132+ - name : Get next version from release drafter
133+ id : get-version
134+ uses : aaronsteers/semantic-pr-release-drafter@v1.1.0
135+ with :
136+ dry-run : true
137+ env :
138+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
139+
140+ - name : Install Speakeasy CLI
141+ run : |
142+ SPEAKEASY_IMAGE=$(yq '.services.speakeasy.image' .github/speakeasy/dummy-compose.yml)
143+ echo "Pinned Speakeasy image: $SPEAKEASY_IMAGE"
144+ docker pull "$SPEAKEASY_IMAGE"
145+ CONTAINER_ID=$(docker create "$SPEAKEASY_IMAGE")
146+ sudo docker cp "$CONTAINER_ID:/usr/local/bin/speakeasy" /usr/local/bin/speakeasy
147+ docker rm "$CONTAINER_ID" >/dev/null
148+ speakeasy --version
149+
150+ - name : Resolve SDK version
151+ id : resolve-version
152+ env :
153+ DRAFTER_VERSION : ${{ steps.get-version.outputs.resolved-version }}
154+ run : |
155+ GENYAML_VERSION=$(yq '.python.version' gen.yaml)
156+ echo "Release drafter version: ${DRAFTER_VERSION:-<empty>}"
157+ echo "gen.yaml version: ${GENYAML_VERSION:-<empty>}"
158+ # Use gen.yaml version if it is a higher major than the drafter
159+ # (handles initial major-version bumps before the first release).
160+ # Otherwise, prefer the release drafter's resolved version.
161+ DRAFTER_MAJOR=${DRAFTER_VERSION%%.*}
162+ GENYAML_MAJOR=${GENYAML_VERSION%%.*}
163+ if [ -n "$GENYAML_VERSION" ] && [ "${GENYAML_MAJOR:-0}" -gt "${DRAFTER_MAJOR:-0}" ]; then
164+ echo "version=${GENYAML_VERSION}" | tee -a $GITHUB_OUTPUT
165+ echo "Using gen.yaml version (higher major: ${GENYAML_MAJOR} > ${DRAFTER_MAJOR})"
166+ elif [ -n "$DRAFTER_VERSION" ]; then
167+ echo "version=${DRAFTER_VERSION}" | tee -a $GITHUB_OUTPUT
168+ echo "Using release drafter version"
169+ elif [ -n "$GENYAML_VERSION" ]; then
170+ echo "version=${GENYAML_VERSION}" | tee -a $GITHUB_OUTPUT
171+ echo "Falling back to gen.yaml version"
172+ else
173+ echo "::error::No version could be resolved from release drafter or gen.yaml."
174+ exit 1
175+ fi
176+
177+ - name : Generate SDK with Speakeasy
178+ env :
179+ SPEAKEASY_API_KEY : ${{ secrets.SPEAKEASY_API_KEY }}
180+ VERSION : ${{ steps.resolve-version.outputs.version }}
181+ run : |
182+ echo "Generating with version: $VERSION"
183+ uvx --from=poethepoet poe generate-code
184+
185+ - name : Post-generation patching
186+ run : python3 scripts/post_generate.py
187+
188+ - name : Verify generated code
189+ run : |
190+ if [ -f "pyproject.toml" ]; then
191+ echo "pyproject.toml found (v2 generator confirmed)"
192+ uv sync --no-install-project 2>/dev/null || true
193+ uv run python -c "import airbyte_api; print(f'SDK import OK: {airbyte_api.__name__}')" || echo "::warning::SDK import check failed"
194+ else
195+ echo "::warning::No pyproject.toml found. Generation may not have produced v2 output."
196+ fi
197+
198+ - name : Upload generated SDK as artifact
199+ if : ${{ inputs.dry_run }}
200+ uses : actions/upload-artifact@v4
201+ with :
202+ name : generated_sdk_code
203+ path : |
204+ src/
205+ pyproject.toml
206+ py.typed
207+ retention-days : 7
208+
209+ - name : Generation Summary
210+ run : |
211+ echo "=== Generation Summary ==="
212+ echo "Source files: $(find src/ -name '*.py' 2>/dev/null | wc -l)"
213+ echo "Model files: $(find src/ -path '*/models/*' -name '*.py' 2>/dev/null | wc -l)"
214+ if [ -f "pyproject.toml" ]; then
215+ echo "Package version: $(grep 'version' pyproject.toml | head -1)"
216+ fi
217+
218+ - name : Check for changes
219+ if : ${{ !inputs.dry_run }}
220+ id : changes
221+ run : |
222+ if [ -n "$(git status --porcelain)" ]; then
223+ echo "has_changes=true" >> $GITHUB_OUTPUT
224+ else
225+ echo "has_changes=false" >> $GITHUB_OUTPUT
226+ fi
227+
228+ # --- PR branch mode: commit and push to the existing PR branch ---
229+ - name : Push regenerated code to PR branch
230+ if : ${{ !inputs.dry_run && github.event.inputs.pr != '' && steps.changes.outputs.has_changes == 'true' }}
231+ run : |
232+ git config user.name "octavia-bot[bot]"
233+ git config user.email "octavia-bot[bot]@users.noreply.github.com"
234+ git add -A
235+ git commit -m "chore: regenerate SDK with Speakeasy"
236+ git push
237+
238+ # --- New PR mode: create a PR to main ---
239+ - name : Create Pull Request
240+ if : ${{ !inputs.dry_run && steps.changes.outputs.has_changes == 'true' && github.event.inputs.pr == '' }}
241+ id : create-pr
242+ uses : peter-evans/create-pull-request@v6
243+ with :
244+ token : ${{ steps.token.outputs.token }}
245+ commit-message : " chore: regenerate SDK with Speakeasy"
246+ title : " chore: regenerate SDK with Speakeasy"
247+ body : |
248+ This PR was automatically generated by the Speakeasy SDK generation workflow.
249+
250+ Please review the changes and merge if they look correct.
251+ branch : speakeasy-sdk-regen
252+ base : main
253+ delete-branch : true
254+
255+ - name : Enable auto-merge (new PR only)
256+ if : |
257+ (github.event_name == 'push'
258+ || github.event_name == 'schedule'
259+ ) && steps.create-pr.outputs.pull-request-operation == 'created'
260+ env :
261+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
262+ run : gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --squash
263+
264+ - name : Append success comment
265+ if : ${{ success() && !inputs.dry_run && github.event.inputs.pr != '' }}
266+ uses : peter-evans/create-or-update-comment@v5
267+ with :
268+ token : ${{ steps.token.outputs.token }}
269+ comment-id : ${{ steps.start-comment.outputs.comment-id }}
270+ reactions : hooray
271+ body : |
272+ > SDK generation completed successfully.
273+
274+ - name : Append failure comment
275+ if : ${{ failure() && !inputs.dry_run && github.event.inputs.pr != '' }}
276+ uses : peter-evans/create-or-update-comment@v5
277+ with :
278+ token : ${{ steps.token.outputs.token }}
279+ comment-id : ${{ steps.start-comment.outputs.comment-id }}
280+ reactions : confused
281+ body : |
282+ > SDK generation failed. Check the [job output](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
0 commit comments