name: Stainless SDK Builds run-name: Build Stainless SDK from OpenAPI spec changes # This workflow uses pull_request_target, which allows it to run on pull requests # from forks with access to secrets. This is safe because the workflow definition # comes from the base branch (trusted), and the action only reads OpenAPI spec # files without executing any code from the PR. on: pull_request_target: types: - opened - synchronize - reopened - closed paths: - "client-sdks/stainless/**" - ".github/workflows/stainless-builds.yml" # this workflow workflow_dispatch: inputs: pr_number: description: 'PR number to run Stainless build for' required: true type: number sdk_install_url: description: 'Python SDK install URL (optional, for testing specific builds)' required: false type: string concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }} cancel-in-progress: true env: # Stainless organization name. STAINLESS_ORG: llamastack # Stainless project name. STAINLESS_PROJECT: llama-stack-client # Path to your OpenAPI spec. OAS_PATH: ./client-sdks/stainless/openapi.yml # Path to your Stainless config. Optional; only provide this if you prefer # to maintain the ground truth Stainless config in your own repo. CONFIG_PATH: ./client-sdks/stainless/config.yml # When to fail the job based on build conclusion. # Options: "never" | "note" | "warning" | "error" | "fatal". FAIL_ON: error # In your repo secrets, configure: # - STAINLESS_API_KEY: a Stainless API key, which you can generate on the # Stainless organization dashboard jobs: compute-branch: runs-on: ubuntu-latest outputs: preview_branch: ${{ steps.compute.outputs.preview_branch }} base_branch: ${{ steps.compute.outputs.base_branch }} merge_branch: ${{ steps.compute.outputs.merge_branch }} pr_head_repo: ${{ steps.compute.outputs.pr_head_repo }} pr_head_ref: ${{ steps.compute.outputs.pr_head_ref }} pr_head_sha: ${{ steps.compute.outputs.pr_head_sha }} pr_base_sha: ${{ steps.compute.outputs.pr_base_sha }} pr_base_ref: ${{ steps.compute.outputs.pr_base_ref }} pr_title: ${{ steps.compute.outputs.pr_title }} is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} steps: - name: Fetch PR details for workflow_dispatch if: github.event_name == 'workflow_dispatch' id: fetch-pr env: GH_TOKEN: ${{ github.token }} run: | PR_DATA=$(gh pr view ${{ inputs.pr_number }} --repo ${{ github.repository }} --json headRefName,headRepository,headRefOid,baseRefName,baseRefOid,headRepositoryOwner,title) echo "pr_data=$PR_DATA" >> $GITHUB_OUTPUT - name: Compute branch names id: compute run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then # Extract from fetched PR data PR_DATA='${{ steps.fetch-pr.outputs.pr_data }}' FORK_OWNER=$(echo "$PR_DATA" | jq -r '.headRepositoryOwner.login') REPO_NAME=$(echo "$PR_DATA" | jq -r '.headRepository.name') HEAD_REPO="${FORK_OWNER}/${REPO_NAME}" BRANCH_NAME=$(echo "$PR_DATA" | jq -r '.headRefName') HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid') BASE_SHA=$(echo "$PR_DATA" | jq -r '.baseRefOid') BASE_REF=$(echo "$PR_DATA" | jq -r '.baseRefName') PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') else # Use pull_request_target event data HEAD_REPO="${{ github.event.pull_request.head.repo.full_name }}" BRANCH_NAME="${{ github.event.pull_request.head.ref }}" FORK_OWNER="${{ github.event.pull_request.head.repo.owner.login }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" BASE_SHA="${{ github.event.pull_request.base.sha }}" BASE_REF="${{ github.event.pull_request.base.ref }}" PR_TITLE="${{ github.event.pull_request.title }}" fi BASE_REPO="${{ github.repository }}" if [ "$HEAD_REPO" != "$BASE_REPO" ]; then # Fork PR: prefix with fork owner for isolation if [ -z "$FORK_OWNER" ]; then echo "Error: Fork PR detected but fork owner is empty" >&2 exit 1 fi PREVIEW_BRANCH="preview/${FORK_OWNER}/${BRANCH_NAME}" BASE_BRANCH="preview/base/${FORK_OWNER}/${BRANCH_NAME}" IS_FORK_PR="true" else # Same-repo PR PREVIEW_BRANCH="preview/${BRANCH_NAME}" BASE_BRANCH="preview/base/${BRANCH_NAME}" IS_FORK_PR="false" fi echo "preview_branch=${PREVIEW_BRANCH}" >> $GITHUB_OUTPUT echo "base_branch=${BASE_BRANCH}" >> $GITHUB_OUTPUT echo "merge_branch=${PREVIEW_BRANCH}" >> $GITHUB_OUTPUT echo "pr_head_repo=${HEAD_REPO}" >> $GITHUB_OUTPUT echo "pr_head_ref=${BRANCH_NAME}" >> $GITHUB_OUTPUT echo "pr_head_sha=${HEAD_SHA}" >> $GITHUB_OUTPUT echo "pr_base_sha=${BASE_SHA}" >> $GITHUB_OUTPUT echo "pr_base_ref=${BASE_REF}" >> $GITHUB_OUTPUT echo "pr_title=${PR_TITLE}" >> $GITHUB_OUTPUT echo "is_fork_pr=${IS_FORK_PR}" >> $GITHUB_OUTPUT preview: needs: compute-branch # Skip preview if workflow_dispatch provides sdk_install_url, or if PR is being closed if: | (github.event_name == 'workflow_dispatch' && inputs.sdk_install_url == '') || (github.event_name == 'pull_request_target' && github.event.action != 'closed') runs-on: ubuntu-latest permissions: contents: read pull-requests: write outputs: sdk_install_url: ${{ fromJSON(steps.run-preview.outputs.outcomes || '{}').python.install_url || '' }} steps: # Checkout the PR's code to access the OpenAPI spec and config files. # This is necessary to read the spec/config from the PR (including from forks). - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: ${{ needs.compute-branch.outputs.pr_head_repo }} ref: ${{ needs.compute-branch.outputs.pr_head_sha }} fetch-depth: 2 - name: Run preview builds id: run-preview uses: stainless-api/upload-openapi-spec-action/preview@979824f1ea5f44334940f0768d04642b6cdaa0d1 # 1.8.1 env: PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }} with: stainless_api_key: ${{ secrets.STAINLESS_API_KEY }} org: ${{ env.STAINLESS_ORG }} project: ${{ env.STAINLESS_PROJECT }} oas_path: ${{ env.OAS_PATH }} config_path: ${{ env.CONFIG_PATH }} fail_on: ${{ env.FAIL_ON }} base_sha: ${{ needs.compute-branch.outputs.pr_base_sha }} base_ref: ${{ needs.compute-branch.outputs.pr_base_ref }} head_sha: ${{ needs.compute-branch.outputs.pr_head_sha }} branch: ${{ needs.compute-branch.outputs.preview_branch }} base_branch: ${{ needs.compute-branch.outputs.base_branch }} commit_message: ${{ needs.compute-branch.outputs.pr_title }} make_comment: true run-integration-tests: needs: [compute-branch, preview] if: | always() && (needs.preview.result == 'success' || needs.preview.result == 'skipped') && (github.event_name == 'workflow_dispatch' || github.event.action != 'closed') uses: ./.github/workflows/integration-tests.yml with: # Use provided sdk_install_url from workflow_dispatch, or from preview build sdk_install_url: ${{ inputs.sdk_install_url || needs.preview.outputs.sdk_install_url }} matrix_key: 'stainless' test-all-client-versions: false pr_head_sha: ${{ needs.compute-branch.outputs.pr_head_sha }} pr_head_ref: ${{ needs.compute-branch.outputs.pr_head_ref }} is_fork_pr: ${{ needs.compute-branch.outputs.is_fork_pr == 'true' }} merge: needs: compute-branch if: github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: # Checkout the PR's code to access the OpenAPI spec and config files. # This is necessary to read the spec/config from the PR (including from forks). - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: ${{ needs.compute-branch.outputs.pr_head_repo }} ref: ${{ needs.compute-branch.outputs.pr_head_sha }} fetch-depth: 2 # Note that this only merges in changes that happened on the last build on # the computed preview branch. It's possible that there are OAS/config # changes that haven't been built, if the preview job didn't finish # before this step starts. In theory we want to wait for all builds # against the preview branch to complete, but assuming that # the preview job happens before the PR merge, it should be fine. - name: Run merge build uses: stainless-api/upload-openapi-spec-action/merge@979824f1ea5f44334940f0768d04642b6cdaa0d1 # 1.8.1 with: stainless_api_key: ${{ secrets.STAINLESS_API_KEY }} org: ${{ env.STAINLESS_ORG }} project: ${{ env.STAINLESS_PROJECT }} oas_path: ${{ env.OAS_PATH }} config_path: ${{ env.CONFIG_PATH }} fail_on: ${{ env.FAIL_ON }} base_sha: ${{ needs.compute-branch.outputs.pr_base_sha }} base_ref: ${{ needs.compute-branch.outputs.pr_base_ref }} head_sha: ${{ needs.compute-branch.outputs.pr_head_sha }} merge_branch: ${{ needs.compute-branch.outputs.merge_branch }}