Skip to content

Commit de2a30d

Browse files
committed
ci: Add nightly canary release for python sdk
1 parent fb80c54 commit de2a30d

10 files changed

Lines changed: 624 additions & 50 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# This workflow schedules the Python SDK canary publish workflow on a daily basis.
2+
# The actual publish still happens in `test-publish-py-sdk.yaml` so TestPyPI trusted
3+
# publishing only needs that one workflow configured as the publisher.
4+
5+
name: Schedule Python SDK Canary Publish
6+
7+
concurrency:
8+
group: publish-py-sdk-canary-scheduler
9+
cancel-in-progress: false
10+
11+
on:
12+
schedule:
13+
- cron: "23 6 * * *"
14+
workflow_dispatch:
15+
16+
jobs:
17+
dispatch-canary-publish:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 5
20+
permissions:
21+
actions: write
22+
steps:
23+
- name: Dispatch TestPyPI publish workflow
24+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
25+
with:
26+
script: |
27+
await github.rest.actions.createWorkflowDispatch({
28+
owner: context.repo.owner,
29+
repo: context.repo.repo,
30+
workflow_id: "test-publish-py-sdk.yaml",
31+
ref: "main",
32+
inputs: {
33+
ref: "main",
34+
release_type: "canary",
35+
dry_run: "false",
36+
},
37+
});
38+
39+
- name: Summarize dispatch
40+
run: |
41+
{
42+
echo "## Python SDK Canary Dispatch Queued"
43+
echo
44+
echo "- Workflow: \`test-publish-py-sdk.yaml\`"
45+
echo "- Release type: \`canary\`"
46+
echo "- Ref: \`main\`"
47+
} >> "$GITHUB_STEP_SUMMARY"

.github/workflows/test-publish-py-sdk.yaml

Lines changed: 197 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
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

75
name: Publish Python SDK to TestPyPI
86

@@ -14,6 +12,14 @@ on:
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
@@ -27,6 +33,9 @@ jobs:
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]

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ make build
133133

134134
Important caveat:
135135

136-
- `py/scripts/template-version.sh` rewrites `py/src/braintrust/version.py` during build.
136+
- `py/scripts/template-version.py` rewrites `py/src/braintrust/version.py` during build.
137137
- `py/Makefile` restores that file afterward with `git checkout`.
138138

139139
Avoid editing `py/src/braintrust/version.py` while also running build commands.

0 commit comments

Comments
 (0)