diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2d99dc32 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,47 @@ +# Dependabot configuration for airbyte-api-python-sdk. +# +# `.speakeasy/workflow.yaml` uses `speakeasyVersion: pinned`, so the actual CLI +# version is pinned in `.github/speakeasy/dummy-compose.yml` and bumped by the +# docker-compose ecosystem entry below. + +version: 2 +updates: + # Speakeasy CLI version pin (image: tag in .github/speakeasy/dummy-compose.yml). + # See that file for the full explanation. + - package-ecosystem: docker-compose + directory: /.github/speakeasy + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + commit-message: + prefix: ci(speakeasy) + labels: + - dependencies + - speakeasy + + # GitHub Actions used in .github/workflows/*.yml + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + commit-message: + prefix: ci + labels: + - dependencies + - github-actions + + # Python dependencies (uv / pyproject.toml) + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + commit-message: + prefix: chore + labels: + - dependencies + - python diff --git a/.github/workflows/generate-command.yml b/.github/workflows/generate-command.yml index b5b0a817..e65cec9f 100644 --- a/.github/workflows/generate-command.yml +++ b/.github/workflows/generate-command.yml @@ -66,29 +66,18 @@ jobs: pull-requests: write steps: - name: Authenticate as GitHub App - if: ${{ !inputs.dry_run && github.event.inputs.pr != '' }} uses: actions/create-github-app-token@v3 - id: get-app-token - continue-on-error: true + id: app-token with: app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - - name: Set working token - id: token - run: | - if [ -n "${{ steps.get-app-token.outputs.token }}" ]; then - echo "token=${{ steps.get-app-token.outputs.token }}" | tee -a $GITHUB_OUTPUT - else - echo "token=${{ github.token }}" | tee -a $GITHUB_OUTPUT - fi - - 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.token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.inputs.pr }} comment-id: ${{ github.event.inputs.comment-id || '' }} body: | @@ -102,7 +91,7 @@ jobs: if: ${{ !inputs.dry_run && github.event.inputs.pr != '' }} id: pr-branch env: - GH_TOKEN: ${{ steps.token.outputs.token }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} PR_NUMBER: ${{ github.event.inputs.pr }} run: | PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}) @@ -119,16 +108,11 @@ jobs: with: fetch-depth: 0 ref: ${{ steps.pr-branch.outputs.head_ref || '' }} - token: ${{ steps.token.outputs.token || github.token }} + token: ${{ steps.app-token.outputs.token }} - name: Install uv uses: astral-sh/setup-uv@v5 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Get next version from release drafter id: get-version uses: aaronsteers/semantic-pr-release-drafter@v1.1.0 @@ -241,7 +225,7 @@ jobs: id: create-pr uses: peter-evans/create-pull-request@v6 with: - token: ${{ steps.token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} commit-message: "chore: regenerate SDK with Speakeasy" title: "chore: regenerate SDK with Speakeasy" body: | @@ -258,14 +242,14 @@ jobs: || github.event_name == 'schedule' ) && steps.create-pr.outputs.pull-request-operation == 'created' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.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.token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} comment-id: ${{ steps.start-comment.outputs.comment-id }} reactions: hooray body: | @@ -275,7 +259,7 @@ jobs: if: ${{ failure() && !inputs.dry_run && github.event.inputs.pr != '' }} uses: peter-evans/create-or-update-comment@v5 with: - token: ${{ steps.token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} comment-id: ${{ steps.start-comment.outputs.comment-id }} reactions: confused body: | diff --git a/.github/workflows/pre-release-command.yml b/.github/workflows/pre-release-command.yml index 519bbc0d..791f88c4 100644 --- a/.github/workflows/pre-release-command.yml +++ b/.github/workflows/pre-release-command.yml @@ -1,7 +1,21 @@ -# Stub: Pre-Release Workflow +# Pre-Release Workflow # -# Minimal placeholder to register the workflow_dispatch trigger on main. -# Full implementation arrives in a follow-up PR. +# Builds and publishes a pre-release version of the Python SDK to PyPI. +# Pre-releases are installable via `pip install airbyte-api==1.0.0rc1` but +# are NOT the default version, so existing users are unaffected. +# +# Triggers: +# - Manual workflow_dispatch: From the Actions tab +# - Slash command: `/pre-release version=1.0.0rc1` on a PR comment +# +# Inputs: +# +# version (REQUIRED): The pre-release version string. +# Must contain a PEP 440 pre-release suffix: rcN, betaN, alphaN, devN. +# Examples: 1.0.0rc1, 1.0.0a1, 1.0.0b1, 1.0.0.dev1 +# +# ref (optional, default: main): The branch, tag, or commit SHA to build from. +# When triggered via slash command on a PR, defaults to the PR's head branch. name: Pre-Release @@ -9,7 +23,9 @@ on: workflow_dispatch: inputs: version: - description: 'Pre-release version (e.g. 1.0.0rc1)' + description: >- + Pre-release version (e.g. 1.0.0rc1, 1.0.0a1). + Must contain a PEP 440 pre-release suffix. required: true type: string ref: @@ -26,10 +42,115 @@ on: required: false type: string +concurrency: + group: pre-release-${{ inputs.version }} + cancel-in-progress: true + +permissions: + contents: read + jobs: - stub: - name: Stub (placeholder) - if: false + pre_release: + name: Build & Publish Pre-Release runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/airbyte-api/ + permissions: + contents: write + pull-requests: write + id-token: write steps: - - run: echo "stub" + # ── Slash command: post starting comment ──────────────────────── + - name: Authenticate as GitHub App + uses: actions/create-github-app-token@v3 + id: app-token + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} + + - name: Post starting comment + if: ${{ inputs.pr != '' }} + id: start-comment + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ inputs.pr }} + comment-id: ${{ inputs.comment-id || '' }} + body: | + > **Pre-Release Job Info** + > + > Building pre-release `${{ inputs.version }}` from ref `${{ inputs.ref || 'PR head branch' }}`. + + > Job started... [Check job output.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + # ── Resolve ref from PR if not explicitly provided ────────────── + - name: Resolve PR head branch + if: ${{ inputs.pr != '' && inputs.ref == 'main' }} + id: resolve-ref + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + PR_HEAD=$(gh pr view "${{ inputs.pr }}" --repo "${{ github.repository }}" --json headRefName -q '.headRefName') + echo "ref=$PR_HEAD" >> "$GITHUB_OUTPUT" + + # ── Validate version input ────────────────────────────────────── + - name: Validate pre-release version + run: | + VERSION="${{ inputs.version }}" + + # PEP 440 pre-release pattern: X.Y.Z(a|b|rc|dev)N + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(a|b|rc|dev|\.dev)[0-9]+$'; then + echo "::error::Invalid version or missing pre-release suffix. Expected PEP 440 format: X.Y.Z(a|b|rc|dev)N (e.g. 1.0.0rc1). Got: $VERSION" + exit 1 + fi + + echo "Pre-release version validated: $VERSION" + + # ── Checkout ──────────────────────────────────────────────────── + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ steps.resolve-ref.outputs.ref || inputs.ref }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + # ── Set version and build ─────────────────────────────────────── + - name: Set pre-release version in pyproject.toml + run: | + VERSION="${{ inputs.version }}" + # Use sed to update version in pyproject.toml + sed -i "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml + echo "Updated pyproject.toml version to: $VERSION" + grep 'version' pyproject.toml | head -1 + + - name: Build package + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # ── Tag the commit ────────────────────────────────────────────── + - name: Create and push tag + run: | + VERSION="${{ inputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v${VERSION}" -m "Pre-release v${VERSION}" + git push origin "v${VERSION}" + + # ── Slash command: post result comment ────────────────────────── + - name: Post result comment + if: ${{ always() && inputs.pr != '' }} + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ inputs.pr }} + body: | + > **Pre-Release Result:** ${{ job.status == 'success' && 'Published' || 'Failed' }} + > + > Version: `${{ inputs.version }}` + > Ref: `${{ steps.resolve-ref.outputs.ref || inputs.ref }}` + > [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ${{ job.status == 'success' && format('> Install: `pip install airbyte-api=={0}`', inputs.version) || '' }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b0856785..ce3d86d5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,15 @@ -# Stub: Publish to PyPI Workflow +# PyPI Publish Workflow # -# Minimal placeholder to register the release trigger on main. -# Full implementation arrives in a follow-up PR. +# Triggered when a GitHub Release is published (draft → published). +# Builds the Python package and uploads it to PyPI using OIDC trusted publishing. +# +# Prerequisites: +# - PyPI trusted publisher configured for this repository: +# https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/ +# Owner: airbytehq +# Repository: airbyte-api-python-sdk +# Workflow: publish.yml +# Environment: pypi name: Publish to PyPI @@ -9,10 +17,29 @@ on: release: types: [published] +permissions: + contents: read + jobs: - stub: - name: Stub (placeholder) - if: false + publish: + name: Build & Publish to PyPI runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/airbyte-api/ + permissions: + id-token: write steps: - - run: echo "stub" + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Build package + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 1bd69422..d45316b5 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,7 +1,17 @@ -# Stub: Release Drafter Workflow +# Release Drafter Workflow # -# Minimal placeholder to register the workflow_dispatch trigger on main. -# Full implementation arrives in a follow-up PR. +# This workflow automatically creates and updates draft releases based on merged PRs. +# It uses semantic PR titles (conventional commits format) to categorize changes. +# +# How it works: +# - On push to main: Updates the draft release with the merged PR +# - Categories are determined by conventional commit type (feat, fix, chore, etc.) +# +# To publish a release: +# 1. Go to the Releases page +# 2. Find the draft release +# 3. Edit the version number if needed +# 4. Click "Publish release" - this creates the git tag and triggers the Publish workflow name: Release Drafter @@ -11,10 +21,37 @@ on: branches: - main +concurrency: + group: release-drafter + cancel-in-progress: true + +permissions: + contents: read + jobs: - stub: - name: Stub (placeholder) - if: false + draft_release: + name: Draft Release + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - - run: echo "stub" + - name: Create draft release + uses: aaronsteers/semantic-pr-release-drafter@v1.1.0 + id: release-drafter + with: + name-template: 'v$RESOLVED_VERSION' + tag-template: 'v$RESOLVED_VERSION' + change-template: '- $TITLE (#$NUMBER)' + template: | + ## Changes + + $CHANGES + + ## Installation + + ```bash + pip install airbyte-api==$RESOLVED_VERSION + ``` + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semantic-pr-title.yml b/.github/workflows/semantic-pr-title.yml index e017149d..e15c830b 100644 --- a/.github/workflows/semantic-pr-title.yml +++ b/.github/workflows/semantic-pr-title.yml @@ -1,7 +1,20 @@ -# Stub: Validate PR Title Workflow +# Semantic PR Title Validation # -# Minimal placeholder to register the pull_request trigger on main. -# Full implementation arrives in a follow-up PR. +# This workflow validates that PR titles follow the Conventional Commits format. +# This is required for the semantic-pr-release-drafter to correctly categorize changes. +# +# Valid formats: +# - feat: Add new feature +# - fix: Fix a bug +# - chore: Maintenance task +# - docs: Documentation changes +# - ci: CI/CD changes +# - refactor: Code refactoring +# - test: Test changes +# - perf: Performance improvements +# - feat!: Breaking change (major version bump) +# +# Optional scope: feat(api): Add new endpoint name: Validate PR Title @@ -9,10 +22,32 @@ on: pull_request: types: [opened, edited, synchronize] +permissions: + pull-requests: read + statuses: write + jobs: - stub: - name: Stub (placeholder) - if: false + validate: + name: Validate Semantic PR Title runs-on: ubuntu-latest steps: - - run: echo "stub" + - name: Validate Semantic PR Title + uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + chore + docs + ci + refactor + test + perf + build + revert + requireScope: false + scopes: "" + wip: true + validateSingleCommit: false diff --git a/.github/workflows/slash-command-dispatch.yml b/.github/workflows/slash-command-dispatch.yml index aa18be01..368cbc89 100644 --- a/.github/workflows/slash-command-dispatch.yml +++ b/.github/workflows/slash-command-dispatch.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Authenticate as GitHub App uses: actions/create-github-app-token@v3 - id: get-app-token + id: app-token with: app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} @@ -29,11 +29,12 @@ jobs: uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5.0.2 with: repository: ${{ github.repository }} - token: ${{ steps.get-app-token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} dispatch-type: workflow issue-type: pull-request commands: | generate + pre-release static-args: | pr=${{ github.event.issue.number }} comment-id=${{ github.event.comment.id }} diff --git a/.github/workflows/test-full.yml b/.github/workflows/test-full.yml index 1965cd16..4d84b50a 100644 --- a/.github/workflows/test-full.yml +++ b/.github/workflows/test-full.yml @@ -1,7 +1,20 @@ -# Stub: Test (Full) Workflow +# Validate Speakeasy Generation (Dry Run) + Zero-Diff Check # -# Minimal placeholder to register the workflow_dispatch trigger on main. -# Full implementation arrives in a follow-up PR. +# This workflow validates that Speakeasy generation can complete successfully +# and that the committed generated code matches what the generation pipeline produces. +# +# Jobs: +# 1. validate: Runs the full generation pipeline in dry-run mode +# 2. zero-diff: Compares the dry-run artifacts against the committed code to detect drift. +# If drift is detected, the check fails and posts a comment telling the author to run /generate. +# +# This workflow calls the main generation workflow with dry_run=true to ensure +# both workflows use the same generation logic. +# +# Note: paths-ignore is NOT used at the workflow level because GitHub treats a +# workflow that never runs as "expected" (pending), which blocks required checks. +# Instead, we filter paths at the job level so skipped jobs report as "skipped" +# (equivalent to "passed" for required checks). name: Test (Full) @@ -9,10 +22,148 @@ on: pull_request: workflow_dispatch: +permissions: + contents: write + pull-requests: write + actions: read + jobs: - stub: - name: Stub (placeholder) - if: false + check-paths: + name: Check Changed Paths runs-on: ubuntu-latest steps: - - run: echo "stub" + - name: Checkout repository + uses: actions/checkout@v4 + - name: Filter changed paths + uses: dorny/paths-filter@v4 + id: filter + with: + filters: | + generation: + - '**' + - '!README.md' + - '!docs/**' + outputs: + should_run: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.generation == 'true' }} + + validate: + name: Validate Generation (Dry Run) + needs: check-paths + if: needs.check-paths.outputs.should_run == 'true' + uses: ./.github/workflows/generate-command.yml + with: + dry_run: true + secrets: inherit + + zero-diff: + name: Zero-Diff Check (Generated Code) + needs: [check-paths, validate] + if: needs.check-paths.outputs.should_run == 'true' && github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Download generated SDK artifact + uses: actions/download-artifact@v8 + with: + name: generated_sdk_code + path: /tmp/generated/ + + - name: Compare generated code against committed code + id: diff-check + run: | + DIFF_SUMMARY="" + + echo "=== Comparing generated SDK code ===" + # Compare src/ directory + if [ -d "src/" ] && [ -d "/tmp/generated/src/" ]; then + while IFS= read -r line; do + # Extract relative path from diff output + FILE=$(echo "$line" | sed 's|^Files ||; s| and /tmp/generated/.*||') + ADDED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0") + REMOVED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0") + DIFF_SUMMARY="${DIFF_SUMMARY}${FILE} (+${ADDED}/-${REMOVED})"$'\n' + done < <(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Files" || true) + + # Check for files only in one side + ONLY_LINES=$(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Only" || true) + if [ -n "$ONLY_LINES" ]; then + while IFS= read -r line; do + DIR=$(echo "$line" | sed 's|^Only in /tmp/generated/||; s|^Only in ||; s|: |/|') + if echo "$line" | grep -q "^Only in /tmp/generated/"; then + DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (new file)"$'\n' + else + DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (deleted)"$'\n' + fi + done <<< "$ONLY_LINES" + fi + elif [ -d "/tmp/generated/src/" ]; then + DIFF_SUMMARY="src/ directory missing in committed code but present in generated output"$'\n' + fi + + # Compare pyproject.toml + if [ -f "/tmp/generated/pyproject.toml" ]; then + TOML_DIFF=$(diff -q pyproject.toml /tmp/generated/pyproject.toml 2>&1 || true) + if [ -n "$TOML_DIFF" ]; then + ADDED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0") + REMOVED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0") + DIFF_SUMMARY="${DIFF_SUMMARY}pyproject.toml (+${ADDED}/-${REMOVED})"$'\n' + fi + fi + + if [ -n "$DIFF_SUMMARY" ]; then + echo "has_diff=true" >> $GITHUB_OUTPUT + echo "::warning::Generated code drift detected. The committed code does not match what the generation pipeline produces." + echo "$DIFF_SUMMARY" + echo "$DIFF_SUMMARY" > /tmp/diff_summary.txt + else + echo "has_diff=false" >> $GITHUB_OUTPUT + echo "Zero-diff check passed. Committed code matches generation output." + fi + + - name: Prepare diff summary + if: steps.diff-check.outputs.has_diff == 'true' + id: diff-summary + run: | + SUMMARY=$(cat /tmp/diff_summary.txt 2>/dev/null || echo "(see job logs for full details)") + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "content<<$EOF" >> $GITHUB_OUTPUT + echo "$SUMMARY" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT + + - name: Find existing drift comment + if: steps.diff-check.outputs.has_diff == 'true' + uses: peter-evans/find-comment@v3 + id: find-drift-comment + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: '' + + - name: Post drift comment on PR + if: steps.diff-check.outputs.has_diff == 'true' + uses: peter-evans/create-or-update-comment@v5 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.find-drift-comment.outputs.comment-id || '' }} + edit-mode: replace + body: | + + **Generated Code Drift Detected** + + The committed code does not match what the generation pipeline produces. + + **To fix:** Comment `/generate` on this PR to regenerate. + + ``` + ${{ steps.diff-summary.outputs.content }} + ``` + + - name: Fail if drift detected + if: steps.diff-check.outputs.has_diff == 'true' + run: | + echo "::error::Generated code drift detected. Run /generate on this PR to fix." + exit 1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8fa3f726 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Agent Guidelines for airbyte-api-python-sdk + +For the full contributor guide (code generation lineage, build commands, release process, and debugging generation drift), see [CONTRIBUTING.md](CONTRIBUTING.md). + +## Key Principle: This Is a Generated Codebase + +Most code under `src/airbyte_api/` is generated by Speakeasy. **Do not hand-edit generated files.** To make durable changes, work at the correct layer (overlay, post-generation script, or upstream spec). See [CONTRIBUTING.md — Code Generation Lineage](CONTRIBUTING.md#code-generation-lineage). + +## Generation Drift + +When a drift CI check fails, download the generated artifacts and compare. See [CONTRIBUTING.md — Debugging Generation Drift Failures](CONTRIBUTING.md#debugging-generation-drift-failures). + +Do not guess or iterate blindly. Download the artifact first. + +## Files You Can Safely Edit + +- `overlays/python_speakeasy.yaml` — Speakeasy overlay +- `scripts/post_generate.py` — Post-generation patch script +- `poe_tasks.toml` — Build task definitions +- `.github/workflows/` — CI workflows +- `.github/dependabot.yml` — Dependabot configuration +- `.github/speakeasy/dummy-compose.yml` — Speakeasy CLI version pin +- `.speakeasy/workflow.yaml` — Speakeasy workflow configuration +- `gen.yaml` — Speakeasy generator configuration +- `CONTRIBUTING.md`, `AGENTS.md`, `README.md` — Documentation + +## Files You Must NOT Edit By Hand + +- `src/airbyte_api/` — Generated by Speakeasy +- `pyproject.toml` — Generated by Speakeasy (version managed by release drafter) +- `py.typed` — Generated by Speakeasy + +## Build Commands + +```bash +uvx --from=poethepoet poe generate-code # Generate SDK from spec +uvx --from=poethepoet poe post-generate # Run post-generation patches +uvx --from=poethepoet poe generate-full # Full pipeline +uvx --from=poethepoet poe lint # Lint checks +uvx --from=poethepoet poe fix # Auto-fix lint/format +uvx --from=poethepoet poe test # Run tests +uvx --from=poethepoet poe typecheck # Type checking +``` + +## Regenerating the SDK + +Use the `/generate` slash command on a PR, or trigger manually from [Actions > Generate SDK](https://github.com/airbytehq/airbyte-api-python-sdk/actions/workflows/generate-command.yml). + +Do not attempt to run `speakeasy run` locally — the `SPEAKEASY_API_KEY` is only available in CI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d585717f..837daa4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,144 @@ # Contributing to This Repository -Thank you for your interest in contributing to this repository. Please note that this repository contains generated code. As such, we do not accept direct changes or pull requests. Instead, we encourage you to follow the guidelines below to report issues and suggest improvements. +## Important: This is a Generated Codebase -## How to Report Issues +> **Note:** This repository contains predominantly generated code. We do not accept direct changes to generated files. Report issues on GitHub or submit fixes to the upstream OpenAPI spec in [airbyte-platform](https://github.com/airbytehq/airbyte-platform). -If you encounter any bugs or have suggestions for improvements, please open an issue on GitHub. When reporting an issue, please provide as much detail as possible to help us reproduce the problem. This includes: +## Code Generation Lineage -- A clear and descriptive title -- Steps to reproduce the issue -- Expected and actual behavior -- Any relevant logs, screenshots, or error messages -- Information about your environment (e.g., operating system, software versions) - - For example can be collected using the `npx envinfo` command from your terminal if you have Node.js installed +The Python SDK is generated through a multi-step pipeline: + +``` +┌──────────────────────────────────────────┐ +│ 1. Upstream OpenAPI Spec │ +│ (airbyte-platform / api_sdk.yaml) │ +└──────────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ 2. Speakeasy Overlay (optional) │ +│ (overlays/python_speakeasy.yaml) │ +└──────────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ 3. Speakeasy Code Generation │ +│ (src/airbyte_api/) │ +└──────────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ 4. Post-Generation Patches │ +│ (scripts/post_generate.py) │ +└──────────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ 5. Package Build & Publish │ +│ (pyproject.toml → PyPI) │ +└──────────────────────────────────────────┘ +``` + +### Step-by-step details + +1. **Upstream OpenAPI Spec** — The source-of-truth API definition lives in `airbyte-platform`: + [`airbyte-api/server-api/src/main/openapi/api_sdk.yaml`](https://github.com/airbytehq/airbyte-platform/blob/main/airbyte-api/server-api/src/main/openapi/api_sdk.yaml) + +2. **Speakeasy Overlay** — Python SDK-specific customizations applied on top of the upstream spec before code generation (currently a no-op placeholder): + [`overlays/python_speakeasy.yaml`](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/overlays/python_speakeasy.yaml) + +3. **Speakeasy Code Generation** — Speakeasy consumes the spec (+ overlay if enabled) and generates the Python SDK in `src/airbyte_api/`. These files should never be edited by hand. + +4. **Post-Generation Patches** — A Python script applies any SDK-specific patches after generation (currently a no-op placeholder): + [`scripts/post_generate.py`](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/scripts/post_generate.py) + +5. **Package Build & Publish** — The generated SDK is built with `uv build` and published to PyPI via OIDC trusted publishing. + +> **Tip:** If you need to change SDK behavior, determine which layer is appropriate: +> - **API changes** → submit to the [upstream OpenAPI spec](https://github.com/airbytehq/airbyte-platform/blob/main/airbyte-api/server-api/src/main/openapi/api_sdk.yaml) +> - **Python SDK-specific schema tweaks** → modify the [overlay](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/overlays/python_speakeasy.yaml) +> - **Post-generation fixes** → modify the [post-generate script](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/scripts/post_generate.py) +> - Then trigger regeneration (see below) + +## For Maintainers + +### Regenerating the SDK + +Use the GitHub Actions workflow: [Actions > Generate SDK](https://github.com/airbytehq/airbyte-api-python-sdk/actions/workflows/generate-command.yml) > Run workflow + +Or comment `/generate` on any PR to regenerate and push results to the PR branch. + +### Release Process + +Releases use a draft-based workflow: + +1. The `release-drafter.yml` workflow runs on every push to main, creating/updating a draft release +2. Review the draft at [Releases](https://github.com/airbytehq/airbyte-api-python-sdk/releases) +3. Edit the draft version if needed +4. Click "Publish release" — this triggers PyPI publication via OIDC -## Issue Triage and Upstream Fixes +### Pre-Release Process -We will review and triage issues as quickly as possible. Our goal is to address bugs and incorporate improvements in the upstream source code. Fixes will be included in the next generation of the generated code. +Pre-releases let you publish an SDK version for testing without making it the default on PyPI. -## Contact +**Two ways to trigger a pre-release:** -If you have any questions or need further assistance, please feel free to reach out by opening an issue. +1. **Slash command on a PR** (recommended for PR-based work): + ``` + /pre-release version=1.0.0rc1 + ``` + This builds from the PR's head branch. -Thank you for your understanding and cooperation! +2. **Manual workflow dispatch** (from the [Actions tab](https://github.com/airbytehq/airbyte-api-python-sdk/actions/workflows/pre-release-command.yml)): + - **version** (required): e.g. `1.0.0rc1`, `1.0.0a1`. Must be valid PEP 440 with a pre-release suffix. + - **ref** (optional, default: `main`): branch, tag, or commit SHA to build from. -The Maintainers +**Using a pre-release:** + +```bash +pip install airbyte-api==1.0.0rc1 +``` + +### Debugging Generation Drift Failures + +When the zero-diff CI check fails, it means the committed code doesn't match what the generation pipeline produces. To debug: + +1. Download the generated artifacts from the failed CI run: + ```bash + gh run download --name generated_sdk_code --dir /tmp/generated_from_ci + ``` + +2. Compare against committed code: + ```bash + diff -rq src/ /tmp/generated_from_ci/src/ + ``` + +3. Fix the root cause at the appropriate layer (overlay, post-generate script, or upstream spec), then comment `/generate` on the PR. + +## Speakeasy CLI Version + +The Speakeasy CLI version is pinned in [`.github/speakeasy/dummy-compose.yml`](https://github.com/airbytehq/airbyte-api-python-sdk/blob/main/.github/speakeasy/dummy-compose.yml) and bumped by Dependabot's `docker-compose` ecosystem. The `.speakeasy/workflow.yaml` uses `speakeasyVersion: pinned` to consume whatever version is installed. + +## Build Tasks + +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/): + +```bash +uvx --from=poethepoet poe generate-code # Generate SDK from spec +uvx --from=poethepoet poe post-generate # Run post-generation patches +uvx --from=poethepoet poe generate-full # Full pipeline (generate + patches) +uvx --from=poethepoet poe lint # Run linting checks +uvx --from=poethepoet poe fix # Auto-fix lint/formatting +uvx --from=poethepoet poe test # Run tests +uvx --from=poethepoet poe typecheck # Run type checking +``` + +## How to Report Issues + +If you encounter bugs or have suggestions, please [open an issue](https://github.com/airbytehq/airbyte-api-python-sdk/issues/new). Include: + +- A clear and descriptive title +- Steps to reproduce the issue +- Expected and actual behavior +- Any relevant logs or error messages +- SDK version and Python version diff --git a/gen.yaml b/gen.yaml index 62974236..5c69467a 100644 --- a/gen.yaml +++ b/gen.yaml @@ -20,7 +20,7 @@ generation: schemas: allOfMergeStrategy: shallowMerge requestBodyFieldName: "" - versioningStrategy: automatic + versioningStrategy: manual persistentEdits: {} tests: generateTests: true