Skip to content

Commit 9fbd145

Browse files
feat!: migrate to Speakeasy Python v2 generator infrastructure
Infrastructure-only changes for the v1 → v2 migration: - gen.yaml: templateVersion v2, version 1.0.0 - .speakeasy/workflow.yaml: pinned CLI, upstream spec fetch - .github/speakeasy/dummy-compose.yml: pins CLI v1.784.0 - .github/workflows/generate-command.yml: full generation workflow - poe_tasks.toml: build tasks - .gitattributes: marks generated directories - overlays/python_speakeasy.yaml: no-op placeholder - scripts/post_generate.py: no-op placeholder Removed legacy: setup.py, RELEASES.md, pylintrc, .genignore, scripts/publish.sh, airbyte-api.openapi.yaml, old Speakeasy workflows Co-Authored-By: AJ Steers <aj@airbyte.io>
1 parent 9da996c commit 9fbd145

17 files changed

Lines changed: 436 additions & 62512 deletions

.genignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

.gitattributes

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1-
# This allows generated code to be indexed correctly
2-
*.py linguist-generated=false
1+
# Generated by Speakeasy — do not hand-edit these paths.
2+
# See CONTRIBUTING.md for the full code-generation lineage.
3+
4+
src/ linguist-generated=true
5+
docs/ linguist-generated=true
6+
README.md linguist-generated=true
7+
README-PYPI.md linguist-generated=true
8+
USAGE.md linguist-generated=true
9+
py.typed linguist-generated=true
10+
uv.lock linguist-generated=true
11+
pyproject.toml linguist-generated=true
12+
13+
# Generated lock/metadata files
14+
.speakeasy/gen.lock linguist-generated=true
15+
.speakeasy/workflow.lock linguist-generated=true
16+
17+
# Generated editor/tool config
18+
.vscode/ linguist-generated=true
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Speakeasy CLI version pin. Bumped by Dependabot's `docker-compose`
2+
# ecosystem; never invoked via `docker compose` (hence `dummy-`).
3+
# Consumed by `.github/workflows/generate-command.yml`, which pulls the
4+
# image and copies `/usr/local/bin/speakeasy` onto the runner.
5+
6+
services:
7+
speakeasy:
8+
image: ghcr.io/speakeasy-api/speakeasy:v1.784.0

.github/workflows/generate-command.yml

Lines changed: 253 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
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

826
name: 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+
3259
jobs:
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.

.github/workflows/speakeasy_sdk_generation.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

.github/workflows/speakeasy_sdk_publish.yml

Lines changed: 0 additions & 14 deletions
This file was deleted.

.gitignore

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
**/__pycache__/
2+
pyrightconfig.json
3+
**/.speakeasy/temp/
4+
**/.speakeasy/logs/
5+
.speakeasy/reports
6+
.env.local
17
.python-version
28
.DS_Store
39
venv/
10+
.venv/
411
src/*.egg-info/
512
__pycache__/
613
.pytest_cache/
7-
.python-version`
814
.env
15+
dist/
16+
build/
17+
*.egg-info/
18+
# Generated OpenAPI spec (fetched fresh each generation run)
19+
*.openapi.yaml

0 commit comments

Comments
 (0)