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/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