1- #
21# This workflow is used to publish the Python SDK to TestPyPI.
32# It mirrors the main release workflow where practical, but it does not create git tags
43# or GitHub Releases.
5- #
64
75name : Publish Python SDK to TestPyPI
86
1412 required : true
1513 type : string
1614 default : " main"
15+ release_type :
16+ description : " Release type to publish to TestPyPI"
17+ required : true
18+ type : choice
19+ options :
20+ - prerelease
21+ - canary
22+ default : prerelease
1723 dry_run :
1824 description : " Validate and build without publishing to TestPyPI"
1925 required : true
2733 outputs :
2834 commit_sha : ${{ steps.validate.outputs.commit_sha }}
2935 dry_run : ${{ steps.validate.outputs.dry_run }}
36+ release_type : ${{ steps.validate.outputs.release_type }}
37+ target_branch : ${{ steps.validate.outputs.target_branch }}
38+ version : ${{ steps.validate.outputs.version }}
3039 steps :
3140 - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
3241 with :
@@ -37,26 +46,38 @@ jobs:
3746 with :
3847 cache : true
3948 experimental : true
40- - name : Resolve commit
49+ - name : Resolve test release metadata
4150 id : validate
4251 run : |
43- echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
52+ mise exec -- python py/scripts/test-publish.py prepare \
53+ "${{ github.event.inputs.release_type }}" \
54+ "${{ github.event.inputs.ref }}" \
55+ --run-number "${{ github.run_number }}" \
56+ --github-output "$GITHUB_OUTPUT"
4457 echo "dry_run=${{ github.event.inputs.dry_run }}" >> "$GITHUB_OUTPUT"
4558
4659 build-and-publish :
4760 needs : validate
4861 runs-on : ubuntu-latest
4962 timeout-minutes : 20
5063 permissions :
64+ actions : read
5165 id-token : write # Required for PyPI trusted publishing
5266
5367 outputs :
54- version : ${{ steps.get_version.outputs.version }}
68+ commit_sha : ${{ steps.publish_status.outputs.commit_sha }}
69+ package_name : ${{ steps.publish_status.outputs.package_name }}
70+ published : ${{ steps.publish_status.outputs.published }}
71+ reason : ${{ steps.publish_status.outputs.reason }}
72+ version : ${{ steps.publish_status.outputs.version }}
5573
5674 env :
5775 COMMIT_SHA : ${{ needs.validate.outputs.commit_sha }}
5876 DRY_RUN : ${{ needs.validate.outputs.dry_run }}
5977 PYPI_REPO : testpypi
78+ RELEASE_TYPE : ${{ needs.validate.outputs.release_type }}
79+ TARGET_BRANCH : ${{ needs.validate.outputs.target_branch }}
80+ VERSION : ${{ needs.validate.outputs.version }}
6081
6182 steps :
6283 - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -68,55 +89,213 @@ jobs:
6889 with :
6990 cache : true
7091 experimental : true
92+ - name : Check whether a new canary is needed
93+ if : env.RELEASE_TYPE == 'canary'
94+ id : should_publish
95+ run : |
96+ mise exec -- python py/scripts/test-publish.py check-canary --github-output "$GITHUB_OUTPUT"
97+ - name : Check Python CI status
98+ if : env.RELEASE_TYPE == 'canary' && steps.should_publish.outputs.should_publish == 'true'
99+ id : ci_status
100+ uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
101+ env :
102+ TARGET_BRANCH : ${{ env.TARGET_BRANCH }}
103+ with :
104+ script : |
105+ const { owner, repo } = context.repo;
106+ const response = await github.rest.actions.listWorkflowRuns({
107+ owner,
108+ repo,
109+ workflow_id: "py.yaml",
110+ branch: process.env.TARGET_BRANCH,
111+ status: "completed",
112+ per_page: 1,
113+ });
114+
115+ const run = response.data.workflow_runs[0];
116+ if (!run) {
117+ core.setOutput("should_publish", "false");
118+ core.setOutput("reason", `No completed py.yaml run found on ${process.env.TARGET_BRANCH}.`);
119+ return;
120+ }
121+
122+ if (run.conclusion !== "success") {
123+ core.setOutput("should_publish", "false");
124+ core.setOutput(
125+ "reason",
126+ `Latest completed py.yaml run on ${process.env.TARGET_BRANCH} concluded with ${run.conclusion} (${run.html_url}).`,
127+ );
128+ return;
129+ }
130+
131+ core.setOutput("should_publish", "true");
132+ core.setOutput(
133+ "reason",
134+ `Latest completed py.yaml run on ${process.env.TARGET_BRANCH} succeeded (${run.html_url}).`,
135+ );
71136 - name : Build and verify
137+ if : env.RELEASE_TYPE != 'canary' || (steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true')
138+ env :
139+ BRAINTRUST_RELEASE_CHANNEL : ${{ env.RELEASE_TYPE }}
140+ BRAINTRUST_VERSION_OVERRIDE : ${{ env.VERSION }}
72141 run : |
73142 mise exec -- make -C py install-dev verify-build
74143 - name : Upload build artifacts
144+ if : env.RELEASE_TYPE != 'canary' || (steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true')
75145 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
76146 with :
77147 name : python-sdk-testpypi-dist
78148 path : py/dist/
79149 retention-days : 5
80- - name : Get version from built wheel
81- id : get_version
82- run : |
83- WHEEL=$(ls py/dist/*.whl | head -n 1)
84- VERSION=$(echo "$WHEEL" | sed -n 's/.*braintrust-\([^-]*\)-.*/\1/p')
85- echo "version=$VERSION" >> $GITHUB_OUTPUT
86150 - name : Publish to TestPyPI
87- if : env.DRY_RUN != 'true'
151+ if : env.DRY_RUN != 'true' && (env.RELEASE_TYPE != 'canary' || (steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true'))
88152 uses : pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
89153 with :
90154 repository-url : https://test.pypi.org/legacy/
91155 packages-dir : py/dist/
92156 - name : Summarize dry run
93157 if : env.DRY_RUN == 'true'
94158 run : |
95- echo "Dry run completed for TestPyPI build from $COMMIT_SHA"
159+ {
160+ echo "## Python SDK TestPyPI Dry Run Completed"
161+ echo
162+ echo "- Release type: \`${RELEASE_TYPE}\`"
163+ echo "- Version: \`${VERSION}\`"
164+ echo "- Commit: \`${COMMIT_SHA}\`"
165+ } >> "$GITHUB_STEP_SUMMARY"
166+ - name : Summarize skipped canary publish
167+ if : env.RELEASE_TYPE == 'canary' && env.DRY_RUN != 'true' && (steps.should_publish.outputs.should_publish != 'true' || steps.ci_status.outputs.should_publish != 'true')
168+ env :
169+ SHOULD_PUBLISH_REASON : ${{ steps.should_publish.outputs.reason }}
170+ CI_REASON : ${{ steps.ci_status.outputs.reason }}
171+ run : |
172+ set -euo pipefail
173+ REASON="${CI_REASON:-$SHOULD_PUBLISH_REASON}"
174+ {
175+ echo "## Python SDK Canary Publish Skipped"
176+ echo
177+ echo "$REASON"
178+ } >> "$GITHUB_STEP_SUMMARY"
179+ - name : Set publish status outputs
180+ id : publish_status
181+ if : always()
182+ env :
183+ SHOULD_PUBLISH : ${{ steps.should_publish.outputs.should_publish }}
184+ CI_SHOULD_PUBLISH : ${{ steps.ci_status.outputs.should_publish }}
185+ SHOULD_PUBLISH_REASON : ${{ steps.should_publish.outputs.reason }}
186+ CI_REASON : ${{ steps.ci_status.outputs.reason }}
187+ run : |
188+ set -euo pipefail
189+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
190+ echo "package_name=braintrust" >> "$GITHUB_OUTPUT"
191+ echo "commit_sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
192+
193+ if [ "${DRY_RUN}" = "true" ]; then
194+ echo "published=false" >> "$GITHUB_OUTPUT"
195+ echo "reason=" >> "$GITHUB_OUTPUT"
196+ exit 0
197+ fi
198+
199+ if [ "${RELEASE_TYPE}" != "canary" ]; then
200+ echo "published=true" >> "$GITHUB_OUTPUT"
201+ echo "reason=" >> "$GITHUB_OUTPUT"
202+ exit 0
203+ fi
204+
205+ if [ "${SHOULD_PUBLISH}" = "true" ] && [ "${CI_SHOULD_PUBLISH}" = "true" ]; then
206+ echo "published=true" >> "$GITHUB_OUTPUT"
207+ echo "reason=" >> "$GITHUB_OUTPUT"
208+ exit 0
209+ fi
210+
211+ REASON="${CI_REASON:-$SHOULD_PUBLISH_REASON}"
212+ echo "published=false" >> "$GITHUB_OUTPUT"
213+ echo "reason=${REASON}" >> "$GITHUB_OUTPUT"
96214
97215 notify-success :
98216 needs : [validate, build-and-publish]
99- if : always() && needs.build-and-publish.result == 'success'
217+ if : always() && needs.build-and-publish.result == 'success' && (needs.validate.outputs.dry_run == 'true' || needs.build-and-publish.outputs.published == 'true')
218+ runs-on : ubuntu-latest
219+ timeout-minutes : 5
220+ steps :
221+ - name : Post to Slack on dry run success
222+ if : needs.validate.outputs.dry_run == 'true'
223+ uses : slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
224+ with :
225+ method : chat.postMessage
226+ token : ${{ secrets.SLACK_BOT_TOKEN }}
227+ payload : |
228+ channel: C0ABHT0SWA2
229+ text: "🧪 Python SDK TestPyPI dry run succeeded"
230+ blocks:
231+ - type: "header"
232+ text:
233+ type: "plain_text"
234+ text: "🧪 Python SDK TestPyPI Dry Run Succeeded"
235+ - type: "section"
236+ text:
237+ type: "mrkdwn"
238+ text: "${{ format('*Mode:* dry run\n*Release type:* {0}\n*Version:* {1}\n*Ref:* {2}\n*Commit:* {3}\n\n<{4}/{5}/actions/runs/{6}|View Run>', needs.validate.outputs.release_type, needs.validate.outputs.version, github.event.inputs.ref, needs.validate.outputs.commit_sha, github.server_url, github.repository, github.run_id) }}"
239+ - name : Post to Slack on prerelease success
240+ if : needs.validate.outputs.dry_run != 'true' && needs.validate.outputs.release_type == 'prerelease'
241+ uses : slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
242+ with :
243+ method : chat.postMessage
244+ token : ${{ secrets.SLACK_BOT_TOKEN }}
245+ payload : |
246+ channel: C0ABHT0SWA2
247+ text: "🧪 Python SDK prerelease v${{ needs.build-and-publish.outputs.version }} published to TestPyPI"
248+ blocks:
249+ - type: "header"
250+ text:
251+ type: "plain_text"
252+ text: "🧪 Python SDK Pre-release Published"
253+ - type: "section"
254+ text:
255+ type: "mrkdwn"
256+ text: "${{ format('*Version:* {0}\n*Ref:* {1}\n*Install:* `pip install -i https://test.pypi.org/simple/ braintrust=={0}`\n*Package:* <https://test.pypi.org/project/braintrust/|braintrust (TestPyPI)>\n\n<{2}/{3}/actions/runs/{4}|View Run>', needs.build-and-publish.outputs.version, github.event.inputs.ref, github.server_url, github.repository, github.run_id) }}"
257+ - name : Post to Slack on canary success
258+ if : needs.validate.outputs.dry_run != 'true' && needs.validate.outputs.release_type == 'canary'
259+ uses : slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
260+ with :
261+ method : chat.postMessage
262+ token : ${{ secrets.SLACK_BOT_TOKEN }}
263+ payload : |
264+ channel: C0ABHT0SWA2
265+ text: "🧪 Python SDK canary ${{ needs.build-and-publish.outputs.version }} published to TestPyPI"
266+ blocks:
267+ - type: "header"
268+ text:
269+ type: "plain_text"
270+ text: "🧪 Python SDK Canary Published"
271+ - type: "section"
272+ text:
273+ type: "mrkdwn"
274+ text: "${{ format('*Version:* {0}\n*Branch:* `{1}`\n*Commit:* `{2}`\n*Install:* `pip install -i https://test.pypi.org/simple/ braintrust=={0}`\n*Package:* <https://test.pypi.org/project/braintrust/|braintrust (TestPyPI)>\n\n<{3}/{4}/actions/runs/{5}|View Run>', needs.build-and-publish.outputs.version, needs.validate.outputs.target_branch, needs.build-and-publish.outputs.commit_sha, github.server_url, github.repository, github.run_id) }}"
275+
276+ notify-skipped :
277+ needs : [validate, build-and-publish]
278+ if : always() && needs.validate.outputs.release_type == 'canary' && needs.validate.outputs.dry_run != 'true' && needs.build-and-publish.result == 'success' && needs.build-and-publish.outputs.published != 'true'
100279 runs-on : ubuntu-latest
101280 timeout-minutes : 5
102281 steps :
103- - name : Post to Slack on success
282+ - name : Post to Slack on intentional skip
104283 uses : slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
105284 with :
106285 method : chat.postMessage
107286 token : ${{ secrets.SLACK_BOT_TOKEN }}
108287 payload : |
109288 channel: C0ABHT0SWA2
110- text: "${{ needs.validate.outputs.dry_run == 'true' && '🧪 Python SDK TestPyPI dry run succeeded' || format('🧪 Python SDK prerelease v{0} published to TestPyPI', needs.build-and-publish.outputs.version) }}"
289+ text: "⏭️ Python SDK canary publish skipped: ${{ needs.build-and-publish.outputs.reason }}"
111290 blocks:
112291 - type: "header"
113292 text:
114293 type: "plain_text"
115- text: "${{ needs.validate.outputs.dry_run == 'true' && '🧪 Python SDK TestPyPI Dry Run Succeeded' || '🧪 Python SDK Pre-release Published' }} "
294+ text: "⏭️ Python SDK Canary Publish Skipped "
116295 - type: "section"
117296 text:
118297 type: "mrkdwn"
119- text: "${{ needs.validate.outputs.dry_run == 'true' && format('*Mode :* dry run \n*Ref :* {0} \n*Commit :* {1 }\n\n<{2 }/{3 }/actions/runs/{4 }|View Run>', github.event.inputs.ref, needs.validate.outputs.commit_sha, github.server_url, github.repository, github.run_id) || format('*Version:* {0}\n*Ref:* {1}\n*Install:* `pip install -i https://test.pypi.org/simple/ braintrust=={0}`\n*Package:* <https://test.pypi.org/project/braintrust/|braintrust (TestPyPI)>\n\n<{2}/{3}/actions/runs/{4}|View Run>' , needs.build-and-publish.outputs.version, github.event.inputs.ref , github.server_url, github.repository, github.run_id) }}"
298+ text: "${{ format('*Branch :* `{0}` \n*Commit :* `{1}` \n*Reason :* {2 }\n\n<{3 }/{4 }/actions/runs/{5 }|View Run>', needs.validate.outputs.target_branch, needs.build-and-publish.outputs.commit_sha , needs.build-and-publish.outputs.reason , github.server_url, github.repository, github.run_id) }}"
120299
121300 notify-failure :
122301 needs : [validate, build-and-publish]
0 commit comments