-
Notifications
You must be signed in to change notification settings - Fork 1.3k
250 lines (231 loc) · 10.9 KB
/
stainless-builds.yml
File metadata and controls
250 lines (231 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
name: Stainless SDK Builds
run-name: Build Stainless SDK from OpenAPI spec changes
# SECURITY NOTE: This workflow uses pull_request_target, which runs with access to
# secrets and a privileged GITHUB_TOKEN even for fork PRs.
#
# Security measures in place:
# 1. The preview and merge jobs only use Stainless actions that read OAS/config files
# without executing arbitrary code from the PR.
#
# 2. The integration tests are called with security flags:
# - matrix_json: Pre-defined matrix to skip generate_ci_matrix.py execution
# - disable_cache: Prevents cache poisoning
# - Composite actions in integration-tests.yml use full repo paths with pinned SHA
# so they're loaded from a trusted commit, not from PR checkout
#
# References:
# - https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
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: ogx
# Stainless project name.
STAINLESS_PROJECT: ogx-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
# Pass PR title via environment variable to prevent shell injection.
# Direct interpolation of ${{ github.event.pull_request.title }} into bash
# allows command substitution via backticks or $() in PR titles.
env:
PR_TITLE_FROM_EVENT: ${{ github.event.pull_request.title }}
HEAD_REF_FROM_EVENT: ${{ github.event.pull_request.head.ref }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# Extract from fetched PR data (jq -r safely handles special characters)
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="$HEAD_REF_FROM_EVENT"
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 }}"
# Use environment variable to prevent shell injection from PR titles
PR_TITLE="$PR_TITLE_FROM_EVENT"
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}"
echo "base_branch=${BASE_BRANCH}"
echo "merge_branch=${PREVIEW_BRANCH}"
echo "pr_head_repo=${HEAD_REPO}"
echo "pr_head_ref=${BRANCH_NAME}"
echo "pr_head_sha=${HEAD_SHA}"
echo "pr_base_sha=${BASE_SHA}"
echo "pr_base_ref=${BASE_REF}"
echo "pr_title=${PR_TITLE}"
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@020053e7fbf853281174bd3029eb3bfa7a54c039 # 1.13.0
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 }}
# Hardcoded matrix avoids running generate_ci_matrix.py from the PR checkout
matrix_json: '{"include":[{"suite":"base","setup":"ollama","inference_mode":"record-if-missing"}]}'
# Disable caching to prevent cache poisoning from fork PRs
disable_cache: true
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@020053e7fbf853281174bd3029eb3bfa7a54c039 # 1.13.0
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 }}