Skip to content

Commit a804b84

Browse files
ci: take ownership of pyproject.toml, add dynamic versioning and poe task composition (#166)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 3bb9d70 commit a804b84

12 files changed

Lines changed: 1141 additions & 176 deletions

.genignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pyproject.toml

.github/workflows/generate-command.yml

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ name: Generate SDK
5454
description: Validate generation without creating a PR
5555
type: boolean
5656
default: false
57+
outputs:
58+
has_changes:
59+
description: Whether the generation produced changes vs committed code
60+
value: ${{ jobs.generate.outputs.has_changes }}
61+
drift_summary:
62+
description: Git diff stat summary when drift is detected
63+
value: ${{ jobs.generate.outputs.drift_summary }}
5764

5865
concurrency:
5966
group: ${{ (github.event_name == 'push' || github.event_name == 'schedule') && 'generate-new-pr' || format('generate-{0}', github.run_id) }}
@@ -64,6 +71,9 @@ jobs:
6471
name: Generate SDK
6572
runs-on: ubuntu-latest
6673
timeout-minutes: 30
74+
outputs:
75+
has_changes: ${{ steps.changes.outputs.has_changes }}
76+
drift_summary: ${{ steps.changes.outputs.drift_summary }}
6777
permissions:
6878
contents: write
6979
pull-requests: write
@@ -161,37 +171,13 @@ jobs:
161171
exit 1
162172
fi
163173
164-
- name: Generate SDK with Speakeasy
174+
- name: Generate SDK
165175
env:
166176
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
167177
VERSION: ${{ steps.resolve-version.outputs.version }}
168178
run: |
169179
echo "Generating with version: $VERSION"
170-
uvx --from=poethepoet poe generate-code
171-
172-
- name: Post-generation patching
173-
run: python3 scripts/post_generate.py
174-
175-
- name: Verify generated code
176-
run: |
177-
if [ -f "pyproject.toml" ]; then
178-
echo "pyproject.toml found (v2 generator confirmed)"
179-
uv sync --no-install-project 2>/dev/null || true
180-
uv run python -c "import airbyte_api; print(f'SDK import OK: {airbyte_api.__name__}')" || echo "::warning::SDK import check failed"
181-
else
182-
echo "::warning::No pyproject.toml found. Generation may not have produced v2 output."
183-
fi
184-
185-
- name: Upload generated SDK as artifact
186-
if: ${{ inputs.dry_run }}
187-
uses: actions/upload-artifact@v7
188-
with:
189-
name: generated_sdk_code
190-
path: |
191-
src/
192-
pyproject.toml
193-
py.typed
194-
retention-days: 7
180+
uv run poe generate-full
195181
196182
- name: Generation Summary
197183
run: |
@@ -203,16 +189,28 @@ jobs:
203189
fi
204190
205191
- name: Check for changes
206-
if: ${{ !inputs.dry_run }}
207192
id: changes
208193
run: |
209-
# Restore workflow.lock to HEAD to ignore non-deterministic
210-
# digest changes that cause infinite generate→merge loops.
194+
# Restore non-deterministic Speakeasy lock files to HEAD
195+
# to ignore digest changes that cause infinite generate→merge loops.
211196
git checkout HEAD -- .speakeasy/workflow.lock 2>/dev/null || true
197+
git checkout HEAD -- .speakeasy/gen.lock 2>/dev/null || true
212198
if [ -n "$(git status --porcelain)" ]; then
213-
echo "has_changes=true" >> $GITHUB_OUTPUT
199+
echo "has_changes=true" | tee -a $GITHUB_OUTPUT
200+
echo "=== Changed files ==="
201+
git status --porcelain
202+
echo
203+
echo "=== Diff stat ==="
204+
SUMMARY=$(git diff --stat)
205+
echo "$SUMMARY"
206+
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
207+
{
208+
echo "drift_summary<<$EOF"
209+
echo "$SUMMARY"
210+
echo "$EOF"
211+
} | tee -a "$GITHUB_OUTPUT"
214212
else
215-
echo "has_changes=false" >> $GITHUB_OUTPUT
213+
echo "has_changes=false" | tee -a $GITHUB_OUTPUT
216214
fi
217215
218216
# --- PR branch mode: commit and push to the existing PR branch ---

.github/workflows/pre-release-command.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,15 @@ jobs:
113113
uses: astral-sh/setup-uv@v5
114114

115115
# ── Set version and build ───────────────────────────────────────
116-
- name: Set pre-release version in pyproject.toml
116+
- name: Set pre-release version
117117
run: |
118118
VERSION="${{ inputs.version }}"
119-
# Use sed to update version in pyproject.toml
120-
sed -i "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml
121-
echo "Updated pyproject.toml version to: $VERSION"
122-
grep 'version' pyproject.toml | head -1
119+
sed -i "s/^__version__: str = \".*\"/__version__: str = \"${VERSION}\"/" src/airbyte_api/_version.py
120+
echo "Updated _version.py to: $VERSION"
121+
grep '__version__' src/airbyte_api/_version.py
123122
124123
- name: Build package
125-
run: uv build
124+
run: uv run poe build
126125

127126
- name: Publish to PyPI
128127
run: uv publish

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
uses: astral-sh/setup-uv@v5
3030

3131
- name: Build package
32-
run: uv build
32+
run: uv run poe build
3333

3434
- name: Publish to PyPI
3535
run: uv publish
Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,32 @@
11
# Semantic PR Title Validation
22
#
3-
# This workflow validates that PR titles follow the Conventional Commits format.
4-
# This is required for the semantic-pr-release-drafter to correctly categorize changes.
5-
#
6-
# Valid formats:
7-
# - feat: Add new feature
8-
# - fix: Fix a bug
9-
# - chore: Maintenance task
10-
# - docs: Documentation changes
11-
# - ci: CI/CD changes
12-
# - refactor: Code refactoring
13-
# - test: Test changes
14-
# - perf: Performance improvements
15-
# - feat!: Breaking change (major version bump)
16-
#
17-
# Optional scope: feat(api): Add new endpoint
3+
# This workflow validates PR titles follow conventional commit format.
184

19-
name: Validate PR Title
5+
name: Semantic PR Title Validation
206

217
on:
228
pull_request:
23-
types: [opened, edited, synchronize]
9+
types: [opened, edited, ready_for_review, synchronize]
2410

2511
permissions:
26-
pull-requests: read
27-
statuses: write
12+
contents: read
13+
pull-requests: write
2814

2915
jobs:
30-
validate:
16+
validate-pr-title:
3117
name: Validate Semantic PR Title
18+
# Skip if 'edited' event but the title wasn't changed (e.g., only description was edited)
19+
if: >
20+
github.event.action != 'edited'
21+
|| (
22+
github.event.changes.title &&
23+
github.event.changes.title.from != ''
24+
)
3225
runs-on: ubuntu-latest
3326
steps:
34-
- name: Validate Semantic PR Title
27+
- name: Check semantic PR title
3528
uses: amannn/action-semantic-pull-request@v6
29+
if: ${{ github.event.pull_request.draft == false }}
3630
env:
3731
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3832
with:
@@ -47,7 +41,14 @@ jobs:
4741
perf
4842
build
4943
revert
50-
requireScope: false
51-
scopes: ""
52-
wip: true
53-
validateSingleCommit: false
44+
style
45+
46+
- name: Check for "do not merge" in PR title
47+
if: ${{ github.event.pull_request.draft == false }}
48+
uses: actions/github-script@v8
49+
with:
50+
script: |
51+
const title = context.payload.pull_request.title.toLowerCase();
52+
if (title.includes('do not merge') || title.includes('do-not-merge')) {
53+
core.setFailed('PR title contains "do not merge" or "do-not-merge". Please remove this before merging.');
54+
}

.github/workflows/test-full.yml

Lines changed: 8 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
# Jobs:
77
# 1. validate: Runs the full generation pipeline in dry-run mode
8-
# 2. zero-diff: Compares the dry-run artifacts against the committed code to detect drift.
8+
# 2. zero-diff: Checks for drift using the validate job's outputs (in-place git status).
99
# If drift is detected, the check fails and posts a comment telling the author to run /generate.
1010
#
1111
# This workflow calls the main generation workflow with dry_run=true to ensure
@@ -25,7 +25,6 @@ on:
2525
permissions:
2626
contents: write
2727
pull-requests: write
28-
actions: read
2928

3029
jobs:
3130
check-paths:
@@ -64,87 +63,27 @@ jobs:
6463
contents: read
6564
pull-requests: write
6665
steps:
67-
- name: Checkout PR branch
68-
uses: actions/checkout@v4
69-
70-
- name: Download generated SDK artifact
71-
uses: actions/download-artifact@v8
72-
with:
73-
name: generated_sdk_code
74-
path: /tmp/generated/
75-
76-
- name: Compare generated code against committed code
77-
id: diff-check
66+
- name: Check for generation drift
67+
id: drift-check
7868
run: |
79-
DIFF_SUMMARY=""
80-
81-
echo "=== Comparing generated SDK code ==="
82-
# Compare src/ directory
83-
if [ -d "src/" ] && [ -d "/tmp/generated/src/" ]; then
84-
while IFS= read -r line; do
85-
# Extract relative path from diff output
86-
FILE=$(echo "$line" | sed 's|^Files ||; s| and /tmp/generated/.*||')
87-
ADDED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0")
88-
REMOVED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0")
89-
DIFF_SUMMARY="${DIFF_SUMMARY}${FILE} (+${ADDED}/-${REMOVED})"$'\n'
90-
done < <(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Files" || true)
91-
92-
# Check for files only in one side
93-
ONLY_LINES=$(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Only" || true)
94-
if [ -n "$ONLY_LINES" ]; then
95-
while IFS= read -r line; do
96-
DIR=$(echo "$line" | sed 's|^Only in /tmp/generated/||; s|^Only in ||; s|: |/|')
97-
if echo "$line" | grep -q "^Only in /tmp/generated/"; then
98-
DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (new file)"$'\n'
99-
else
100-
DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (deleted)"$'\n'
101-
fi
102-
done <<< "$ONLY_LINES"
103-
fi
104-
elif [ -d "/tmp/generated/src/" ]; then
105-
DIFF_SUMMARY="src/ directory missing in committed code but present in generated output"$'\n'
106-
fi
107-
108-
# Compare pyproject.toml
109-
if [ -f "/tmp/generated/pyproject.toml" ]; then
110-
TOML_DIFF=$(diff -q pyproject.toml /tmp/generated/pyproject.toml 2>&1 || true)
111-
if [ -n "$TOML_DIFF" ]; then
112-
ADDED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0")
113-
REMOVED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0")
114-
DIFF_SUMMARY="${DIFF_SUMMARY}pyproject.toml (+${ADDED}/-${REMOVED})"$'\n'
115-
fi
116-
fi
117-
118-
if [ -n "$DIFF_SUMMARY" ]; then
69+
if [ "${{ needs.validate.outputs.has_changes }}" = "true" ]; then
11970
echo "has_diff=true" >> $GITHUB_OUTPUT
12071
echo "::warning::Generated code drift detected. The committed code does not match what the generation pipeline produces."
121-
echo "$DIFF_SUMMARY"
122-
echo "$DIFF_SUMMARY" > /tmp/diff_summary.txt
12372
else
12473
echo "has_diff=false" >> $GITHUB_OUTPUT
12574
echo "Zero-diff check passed. Committed code matches generation output."
12675
fi
12776
128-
- name: Prepare diff summary
129-
if: steps.diff-check.outputs.has_diff == 'true'
130-
id: diff-summary
131-
run: |
132-
SUMMARY=$(cat /tmp/diff_summary.txt 2>/dev/null || echo "(see job logs for full details)")
133-
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
134-
echo "content<<$EOF" >> $GITHUB_OUTPUT
135-
echo "$SUMMARY" >> $GITHUB_OUTPUT
136-
echo "$EOF" >> $GITHUB_OUTPUT
137-
13877
- name: Find existing drift comment
139-
if: steps.diff-check.outputs.has_diff == 'true'
78+
if: steps.drift-check.outputs.has_diff == 'true'
14079
uses: peter-evans/find-comment@v3
14180
id: find-drift-comment
14281
with:
14382
issue-number: ${{ github.event.pull_request.number }}
14483
body-includes: '<!-- zero-diff-check -->'
14584

14685
- name: Post drift comment on PR
147-
if: steps.diff-check.outputs.has_diff == 'true'
86+
if: steps.drift-check.outputs.has_diff == 'true'
14887
uses: peter-evans/create-or-update-comment@v5
14988
with:
15089
issue-number: ${{ github.event.pull_request.number }}
@@ -159,11 +98,11 @@ jobs:
15998
**To fix:** Comment `/generate` on this PR to regenerate.
16099
161100
```
162-
${{ steps.diff-summary.outputs.content }}
101+
${{ needs.validate.outputs.drift_summary }}
163102
```
164103
165104
- name: Fail if drift detected
166-
if: steps.diff-check.outputs.has_diff == 'true'
105+
if: steps.drift-check.outputs.has_diff == 'true'
167106
run: |
168107
echo "::error::Generated code drift detected. Run /generate on this PR to fix."
169108
exit 1

AGENTS.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,23 @@ Do not guess or iterate blindly. Download the artifact first.
2222
- `.github/speakeasy/dummy-compose.yml` — Speakeasy CLI version pin
2323
- `.speakeasy/workflow.yaml` — Speakeasy workflow configuration
2424
- `gen.yaml` — Speakeasy generator configuration
25+
- `pyproject.toml` — Human-managed (excluded from Speakeasy via `.genignore`)
2526
- `CONTRIBUTING.md`, `AGENTS.md`, `README.md` — Documentation
2627

2728
## Files You Must NOT Edit By Hand
2829

2930
- `src/airbyte_api/` — Generated by Speakeasy
30-
- `pyproject.toml` — Generated by Speakeasy (version managed by release drafter)
3131
- `py.typed` — Generated by Speakeasy
3232

3333
## Build Commands
3434

3535
```bash
36-
uvx --from=poethepoet poe generate-code # Generate SDK from spec
37-
uvx --from=poethepoet poe post-generate # Run post-generation patches
38-
uvx --from=poethepoet poe generate-full # Full pipeline
39-
uvx --from=poethepoet poe lint # Lint checks
40-
uvx --from=poethepoet poe fix # Auto-fix lint/format
41-
uvx --from=poethepoet poe test # Run tests
42-
uvx --from=poethepoet poe typecheck # Type checking
36+
uv run poe generate-full # Full pipeline (generate + readme + patches)
37+
uv run poe build # Build the Python package
38+
uv run poe lint # Lint checks
39+
uv run poe fix # Auto-fix lint/format
40+
uv run poe test # Run tests
41+
uv run poe typecheck # Type checking
4342
```
4443

4544
## Regenerating the SDK

CONTRIBUTING.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,12 @@ The Speakeasy CLI version is pinned in [`.github/speakeasy/dummy-compose.yml`](h
124124
Build tasks are defined in [`poe_tasks.toml`](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/poe_tasks.toml) and run via [poethepoet](https://poethepoet.natn.io/):
125125

126126
```bash
127-
uvx --from=poethepoet poe generate-code # Generate SDK from spec
128-
uvx --from=poethepoet poe post-generate # Run post-generation patches
129-
uvx --from=poethepoet poe generate-full # Full pipeline (generate + patches)
130-
uvx --from=poethepoet poe lint # Run linting checks
131-
uvx --from=poethepoet poe fix # Auto-fix lint/formatting
132-
uvx --from=poethepoet poe test # Run tests
133-
uvx --from=poethepoet poe typecheck # Run type checking
127+
uv run poe generate-full # Full pipeline (generate + readme + patches)
128+
uv run poe build # Build the Python package
129+
uv run poe lint # Run linting checks
130+
uv run poe fix # Auto-fix lint/formatting
131+
uv run poe test # Run tests
132+
uv run poe typecheck # Run type checking
134133
```
135134

136135
## How to Report Issues

0 commit comments

Comments
 (0)