From 978fdf47b02ff374d8f32823bbab801bc62e57ce Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 20:43:26 +0800 Subject: [PATCH 1/8] Notify SDK repositories when the API spec is published --- .github/scripts/dispatch_sdk_updates.py | 65 ++++++++++++++++++++ .github/scripts/test_dispatch_sdk_updates.py | 39 ++++++++++++ .github/sdk-consumers.json | 5 ++ .github/workflows/dispatch-sdk-updates.yml | 49 +++++++++++++++ .github/workflows/validate-openapi.yml | 3 + .gitignore | 1 + README.md | 37 +++++++++++ 7 files changed, 199 insertions(+) create mode 100644 .github/scripts/dispatch_sdk_updates.py create mode 100644 .github/scripts/test_dispatch_sdk_updates.py create mode 100644 .github/sdk-consumers.json create mode 100644 .github/workflows/dispatch-sdk-updates.yml create mode 100644 .gitignore diff --git a/.github/scripts/dispatch_sdk_updates.py b/.github/scripts/dispatch_sdk_updates.py new file mode 100644 index 0000000..0f2d40b --- /dev/null +++ b/.github/scripts/dispatch_sdk_updates.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Notify registered SDK repositories of a published OpenAPI commit.""" + +import argparse +import json +import os +from pathlib import Path +import re +import urllib.request + +SOURCE = "braintrustdata/braintrust-openapi" +EVENT = "braintrust-openapi-updated" +REGISTRY = Path(__file__).resolve().parents[1] / "sdk-consumers.json" + + +def repositories(path=REGISTRY): + values = json.loads(path.read_text())["repositories"] + if not isinstance(values, list) or not values or any( + not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", value) + for value in values + ) or len(set(values)) != len(values): + raise ValueError("SDK registry must contain unique repository names in braintrustdata") + return values + + +def request_for(repository, spec_sha, token): + if repository not in repositories(): + raise ValueError("repository is not registered as a spec consumer") + if not re.fullmatch(r"[0-9a-f]{40}", spec_sha): + raise ValueError("expected a full commit SHA") + payload = { + "event_type": EVENT, + "client_payload": { + "contract_version": "1", + "spec_repository": SOURCE, + "spec_sha": spec_sha, + }, + } + return urllib.request.Request( + f"https://api.github.com/repos/braintrustdata/{repository}/dispatches", + data=json.dumps(payload).encode(), method="POST", + headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", + "Content-Type": "application/json", "X-GitHub-Api-Version": "2022-11-28"}, + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--list", action="store_true") + parser.add_argument("--repository") + parser.add_argument("--spec-sha") + args = parser.parse_args() + if args.list: + print(json.dumps(repositories())) + return + if not args.repository or not args.spec_sha: + parser.error("--repository and --spec-sha are required when dispatching") + request = request_for(args.repository, args.spec_sha, os.environ["GH_TOKEN"]) + with urllib.request.urlopen(request, timeout=30) as response: + response.read() + print(f"Notified {args.repository} of {SOURCE}@{args.spec_sha}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/test_dispatch_sdk_updates.py b/.github/scripts/test_dispatch_sdk_updates.py new file mode 100644 index 0000000..bb2a8dd --- /dev/null +++ b/.github/scripts/test_dispatch_sdk_updates.py @@ -0,0 +1,39 @@ +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +module_spec = importlib.util.spec_from_file_location("dispatch", Path(__file__).with_name("dispatch_sdk_updates.py")) +dispatch = importlib.util.module_from_spec(module_spec) +module_spec.loader.exec_module(dispatch) + + +class DispatchTests(unittest.TestCase): + def test_notification_matches_the_sdk_contract(self): + request = dispatch.request_for("braintrust-sdk-rust", "a" * 40, "test-token") + self.assertEqual(request.method, "POST") + self.assertEqual(request.full_url, "https://api.github.com/repos/braintrustdata/braintrust-sdk-rust/dispatches") + self.assertEqual(request.get_header("Authorization"), "Bearer test-token") + self.assertEqual(json.loads(request.data), { + "event_type": "braintrust-openapi-updated", + "client_payload": {"contract_version": "1", "spec_repository": "braintrustdata/braintrust-openapi", + "spec_sha": "a" * 40}, + }) + + def test_unregistered_target_and_floating_ref_are_rejected(self): + for repository, sha in [("other-sdk", "a" * 40), ("braintrust-sdk-rust", "main")]: + with self.assertRaises(ValueError): + dispatch.request_for(repository, sha, "test-token") + + def test_registry_rejects_duplicates_and_repository_paths(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "consumers.json" + for values in [["sdk", "sdk"], ["other/repo"], [], "sdk"]: + path.write_text(json.dumps({"repositories": values})) + with self.assertRaises(ValueError): + dispatch.repositories(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/sdk-consumers.json b/.github/sdk-consumers.json new file mode 100644 index 0000000..24dd61f --- /dev/null +++ b/.github/sdk-consumers.json @@ -0,0 +1,5 @@ +{ + "repositories": [ + "braintrust-sdk-rust" + ] +} diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml new file mode 100644 index 0000000..392877e --- /dev/null +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -0,0 +1,49 @@ +name: Notify SDKs of published spec updates + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + consumers: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + outputs: + repositories: ${{ steps.registry.outputs.repositories }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Read SDK registry + id: registry + run: echo "repositories=$(python3 .github/scripts/dispatch_sdk_updates.py --list)" >> "$GITHUB_OUTPUT" + + dispatch: + needs: consumers + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + repository: ${{ fromJSON(needs.consumers.outputs.repositories) }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Create scoped notification token + id: bot + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ vars.GH_BOT_APP_ID || '1394756' }} + private-key: ${{ secrets.GH_BOT_APP_PRIVATE_KEY }} + owner: braintrustdata + repositories: ${{ matrix.repository }} + permission-contents: write + - name: Notify SDK receiver + env: + GH_TOKEN: ${{ steps.bot.outputs.token }} + SDK_REPOSITORY: ${{ matrix.repository }} + SPEC_SHA: ${{ github.sha }} + run: | + python3 .github/scripts/dispatch_sdk_updates.py \ + --repository "$SDK_REPOSITORY" \ + --spec-sha "$SPEC_SHA" diff --git a/.github/workflows/validate-openapi.yml b/.github/workflows/validate-openapi.yml index 4ca530c..8379358 100644 --- a/.github/workflows/validate-openapi.yml +++ b/.github/workflows/validate-openapi.yml @@ -23,3 +23,6 @@ jobs: assert str(spec.get('openapi', '')).startswith('3.'), 'Expected OpenAPI 3 specification' assert isinstance(spec.get('paths'), dict) and spec['paths'], 'Specification has no paths' PY + + - name: Test SDK notification contract + run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README.md b/README.md index 4911722..56ec195 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,40 @@ Braintrust's API. JSON and YAML versions of the spec can be found in the See the [Braintrust docs](https://www.braintrustdata.com/docs/api/spec) for documentation of the spec and more resources. + +## SDK update notifications + +Every push to `main` notifies repositories registered in +[`.github/sdk-consumers.json`](.github/sdk-consumers.json). Each repository owns +its update workflow and PR structure. Initially only the Rust SDK is registered. +No change in spec content means the receiver has no update to propose. + +The notification is a `repository_dispatch` event named +`braintrust-openapi-updated` with this versioned `client_payload`: + +```json +{ + "contract_version": "1", + "spec_repository": "braintrustdata/braintrust-openapi", + "spec_sha": "" +} +``` + +To add an SDK, merge its receiver on its default branch, then add its repository +name to the registry. Receivers should verify the source repository and commit, +handle duplicate/delayed notifications, and open or update one PR for changed +spec content. SDK CI should validate generation from the proposed revision. + +The dispatcher uses `repository_dispatch` because the existing Braintrust bot +has Contents write permission; it does not require adding Actions write +permission to the app. `GH_BOT_APP_PRIVATE_KEY` must be available to this repo, +and the app must be installed on each recipient with Contents write access. +The public App ID defaults to `1394756`; override the repository variable +`GH_BOT_APP_ID` if another app is used. Receiver credentials for opening PRs +are configured separately in each SDK repo. + +Install and configure the Rust receiver before merging the dispatcher. Configure +the bot credentials in both repositories before activation. Notifications run +independently per SDK, so a failure does not cancel the other recipients. The +workflow can be run manually on `main` to retry notification of the latest +published revision. No SDK releases or auto-merges are performed. From 01c02ff6a1826995a8f60d3adcc45bde1b8115e3 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 20:49:04 +0800 Subject: [PATCH 2/8] Send only the published spec commit to SDK receivers --- .github/scripts/dispatch_sdk_updates.py | 2 -- .github/scripts/test_dispatch_sdk_updates.py | 3 +-- README.md | 7 ++++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/scripts/dispatch_sdk_updates.py b/.github/scripts/dispatch_sdk_updates.py index 0f2d40b..0bee8b6 100644 --- a/.github/scripts/dispatch_sdk_updates.py +++ b/.github/scripts/dispatch_sdk_updates.py @@ -31,8 +31,6 @@ def request_for(repository, spec_sha, token): payload = { "event_type": EVENT, "client_payload": { - "contract_version": "1", - "spec_repository": SOURCE, "spec_sha": spec_sha, }, } diff --git a/.github/scripts/test_dispatch_sdk_updates.py b/.github/scripts/test_dispatch_sdk_updates.py index bb2a8dd..3038d97 100644 --- a/.github/scripts/test_dispatch_sdk_updates.py +++ b/.github/scripts/test_dispatch_sdk_updates.py @@ -17,8 +17,7 @@ def test_notification_matches_the_sdk_contract(self): self.assertEqual(request.get_header("Authorization"), "Bearer test-token") self.assertEqual(json.loads(request.data), { "event_type": "braintrust-openapi-updated", - "client_payload": {"contract_version": "1", "spec_repository": "braintrustdata/braintrust-openapi", - "spec_sha": "a" * 40}, + "client_payload": {"spec_sha": "a" * 40}, }) def test_unregistered_target_and_floating_ref_are_rejected(self): diff --git a/README.md b/README.md index 56ec195..39d4213 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,17 @@ its update workflow and PR structure. Initially only the Rust SDK is registered. No change in spec content means the receiver has no update to propose. The notification is a `repository_dispatch` event named -`braintrust-openapi-updated` with this versioned `client_payload`: +`braintrust-openapi-updated` with this `client_payload`: ```json { - "contract_version": "1", - "spec_repository": "braintrustdata/braintrust-openapi", "spec_sha": "" } ``` +Receivers hard-code `braintrustdata/braintrust-openapi` as the source repository; +notifications supply only a commit SHA. + To add an SDK, merge its receiver on its default branch, then add its repository name to the registry. Receivers should verify the source repository and commit, handle duplicate/delayed notifications, and open or update one PR for changed From 9251a9194db5dcbf1963f0217207d78f644963af Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:04:48 +0800 Subject: [PATCH 3/8] Use Bash and jq for SDK notifications and spec validation --- .github/scripts/dispatch_sdk_updates.py | 63 -------------------- .github/scripts/test_dispatch_sdk_updates.py | 38 ------------ .github/workflows/dispatch-sdk-updates.yml | 15 +++-- .github/workflows/validate-openapi.yml | 18 +++--- .gitignore | 1 - 5 files changed, 19 insertions(+), 116 deletions(-) delete mode 100644 .github/scripts/dispatch_sdk_updates.py delete mode 100644 .github/scripts/test_dispatch_sdk_updates.py delete mode 100644 .gitignore diff --git a/.github/scripts/dispatch_sdk_updates.py b/.github/scripts/dispatch_sdk_updates.py deleted file mode 100644 index 0bee8b6..0000000 --- a/.github/scripts/dispatch_sdk_updates.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""Notify registered SDK repositories of a published OpenAPI commit.""" - -import argparse -import json -import os -from pathlib import Path -import re -import urllib.request - -SOURCE = "braintrustdata/braintrust-openapi" -EVENT = "braintrust-openapi-updated" -REGISTRY = Path(__file__).resolve().parents[1] / "sdk-consumers.json" - - -def repositories(path=REGISTRY): - values = json.loads(path.read_text())["repositories"] - if not isinstance(values, list) or not values or any( - not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", value) - for value in values - ) or len(set(values)) != len(values): - raise ValueError("SDK registry must contain unique repository names in braintrustdata") - return values - - -def request_for(repository, spec_sha, token): - if repository not in repositories(): - raise ValueError("repository is not registered as a spec consumer") - if not re.fullmatch(r"[0-9a-f]{40}", spec_sha): - raise ValueError("expected a full commit SHA") - payload = { - "event_type": EVENT, - "client_payload": { - "spec_sha": spec_sha, - }, - } - return urllib.request.Request( - f"https://api.github.com/repos/braintrustdata/{repository}/dispatches", - data=json.dumps(payload).encode(), method="POST", - headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", - "Content-Type": "application/json", "X-GitHub-Api-Version": "2022-11-28"}, - ) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--list", action="store_true") - parser.add_argument("--repository") - parser.add_argument("--spec-sha") - args = parser.parse_args() - if args.list: - print(json.dumps(repositories())) - return - if not args.repository or not args.spec_sha: - parser.error("--repository and --spec-sha are required when dispatching") - request = request_for(args.repository, args.spec_sha, os.environ["GH_TOKEN"]) - with urllib.request.urlopen(request, timeout=30) as response: - response.read() - print(f"Notified {args.repository} of {SOURCE}@{args.spec_sha}") - - -if __name__ == "__main__": - main() diff --git a/.github/scripts/test_dispatch_sdk_updates.py b/.github/scripts/test_dispatch_sdk_updates.py deleted file mode 100644 index 3038d97..0000000 --- a/.github/scripts/test_dispatch_sdk_updates.py +++ /dev/null @@ -1,38 +0,0 @@ -import importlib.util -import json -from pathlib import Path -import tempfile -import unittest - -module_spec = importlib.util.spec_from_file_location("dispatch", Path(__file__).with_name("dispatch_sdk_updates.py")) -dispatch = importlib.util.module_from_spec(module_spec) -module_spec.loader.exec_module(dispatch) - - -class DispatchTests(unittest.TestCase): - def test_notification_matches_the_sdk_contract(self): - request = dispatch.request_for("braintrust-sdk-rust", "a" * 40, "test-token") - self.assertEqual(request.method, "POST") - self.assertEqual(request.full_url, "https://api.github.com/repos/braintrustdata/braintrust-sdk-rust/dispatches") - self.assertEqual(request.get_header("Authorization"), "Bearer test-token") - self.assertEqual(json.loads(request.data), { - "event_type": "braintrust-openapi-updated", - "client_payload": {"spec_sha": "a" * 40}, - }) - - def test_unregistered_target_and_floating_ref_are_rejected(self): - for repository, sha in [("other-sdk", "a" * 40), ("braintrust-sdk-rust", "main")]: - with self.assertRaises(ValueError): - dispatch.request_for(repository, sha, "test-token") - - def test_registry_rejects_duplicates_and_repository_paths(self): - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "consumers.json" - for values in [["sdk", "sdk"], ["other/repo"], [], "sdk"]: - path.write_text(json.dumps({"repositories": values})) - with self.assertRaises(ValueError): - dispatch.repositories(path) - - -if __name__ == "__main__": - unittest.main() diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml index 392877e..95427cd 100644 --- a/.github/workflows/dispatch-sdk-updates.yml +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -18,7 +18,13 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Read SDK registry id: registry - run: echo "repositories=$(python3 .github/scripts/dispatch_sdk_updates.py --list)" >> "$GITHUB_OUTPUT" + run: | + set -euo pipefail + jq -e '.repositories | type == "array" and length > 0 and + length == (unique | length) and + all(.[]; type == "string" and test("^[A-Za-z0-9][A-Za-z0-9_.-]*$"))' \ + .github/sdk-consumers.json >/dev/null + echo "repositories=$(jq -c '.repositories' .github/sdk-consumers.json)" >> "$GITHUB_OUTPUT" dispatch: needs: consumers @@ -44,6 +50,7 @@ jobs: SDK_REPOSITORY: ${{ matrix.repository }} SPEC_SHA: ${{ github.sha }} run: | - python3 .github/scripts/dispatch_sdk_updates.py \ - --repository "$SDK_REPOSITORY" \ - --spec-sha "$SPEC_SHA" + set -euo pipefail + jq -n --arg sha "$SPEC_SHA" \ + '{event_type: "braintrust-openapi-updated", client_payload: {spec_sha: $sha}}' | + gh api --method POST "repos/braintrustdata/$SDK_REPOSITORY/dispatches" --input - diff --git a/.github/workflows/validate-openapi.yml b/.github/workflows/validate-openapi.yml index 8379358..cf89628 100644 --- a/.github/workflows/validate-openapi.yml +++ b/.github/workflows/validate-openapi.yml @@ -15,14 +15,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Validate JSON spec structure run: | - python - <<'PY' - import json - from pathlib import Path + jq -e '(.openapi | type == "string" and startswith("3.")) and + (.paths | type == "object" and length > 0)' openapi/spec.json >/dev/null - spec = json.loads(Path('openapi/spec.json').read_text()) - assert str(spec.get('openapi', '')).startswith('3.'), 'Expected OpenAPI 3 specification' - assert isinstance(spec.get('paths'), dict) and spec['paths'], 'Specification has no paths' - PY - - - name: Test SDK notification contract - run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v + - name: Validate SDK registry + run: | + jq -e '.repositories | type == "array" and length > 0 and + length == (unique | length) and + all(.[]; type == "string" and test("^[A-Za-z0-9][A-Za-z0-9_.-]*$"))' \ + .github/sdk-consumers.json >/dev/null diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c18dd8d..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ From e89900b6444c0f9a838680e2547cea4d2d49cd79 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:13:19 +0800 Subject: [PATCH 4/8] Simplify SDK notification targets and restore spec validation --- .github/sdk-consumers.json | 5 ----- .github/workflows/dispatch-sdk-updates.yml | 21 ++------------------- .github/workflows/validate-openapi.yml | 15 +++++++-------- README.md | 8 ++++---- 4 files changed, 13 insertions(+), 36 deletions(-) delete mode 100644 .github/sdk-consumers.json diff --git a/.github/sdk-consumers.json b/.github/sdk-consumers.json deleted file mode 100644 index 24dd61f..0000000 --- a/.github/sdk-consumers.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "repositories": [ - "braintrust-sdk-rust" - ] -} diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml index 95427cd..cd1e31d 100644 --- a/.github/workflows/dispatch-sdk-updates.yml +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -9,30 +9,13 @@ permissions: contents: read jobs: - consumers: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - outputs: - repositories: ${{ steps.registry.outputs.repositories }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Read SDK registry - id: registry - run: | - set -euo pipefail - jq -e '.repositories | type == "array" and length > 0 and - length == (unique | length) and - all(.[]; type == "string" and test("^[A-Za-z0-9][A-Za-z0-9_.-]*$"))' \ - .github/sdk-consumers.json >/dev/null - echo "repositories=$(jq -c '.repositories' .github/sdk-consumers.json)" >> "$GITHUB_OUTPUT" - dispatch: - needs: consumers + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest strategy: fail-fast: false matrix: - repository: ${{ fromJSON(needs.consumers.outputs.repositories) }} + repository: [braintrust-sdk-rust] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Create scoped notification token diff --git a/.github/workflows/validate-openapi.yml b/.github/workflows/validate-openapi.yml index cf89628..4ca530c 100644 --- a/.github/workflows/validate-openapi.yml +++ b/.github/workflows/validate-openapi.yml @@ -15,12 +15,11 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Validate JSON spec structure run: | - jq -e '(.openapi | type == "string" and startswith("3.")) and - (.paths | type == "object" and length > 0)' openapi/spec.json >/dev/null + python - <<'PY' + import json + from pathlib import Path - - name: Validate SDK registry - run: | - jq -e '.repositories | type == "array" and length > 0 and - length == (unique | length) and - all(.[]; type == "string" and test("^[A-Za-z0-9][A-Za-z0-9_.-]*$"))' \ - .github/sdk-consumers.json >/dev/null + spec = json.loads(Path('openapi/spec.json').read_text()) + assert str(spec.get('openapi', '')).startswith('3.'), 'Expected OpenAPI 3 specification' + assert isinstance(spec.get('paths'), dict) and spec['paths'], 'Specification has no paths' + PY diff --git a/README.md b/README.md index 39d4213..b53df4b 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ documentation of the spec and more resources. ## SDK update notifications -Every push to `main` notifies repositories registered in -[`.github/sdk-consumers.json`](.github/sdk-consumers.json). Each repository owns -its update workflow and PR structure. Initially only the Rust SDK is registered. +Every push to `main` notifies repositories listed in the matrix in +[the dispatch workflow](.github/workflows/dispatch-sdk-updates.yml). Each repository owns +its update workflow and PR structure. Initially only the Rust SDK is listed. No change in spec content means the receiver has no update to propose. The notification is a `repository_dispatch` event named @@ -27,7 +27,7 @@ Receivers hard-code `braintrustdata/braintrust-openapi` as the source repository notifications supply only a commit SHA. To add an SDK, merge its receiver on its default branch, then add its repository -name to the registry. Receivers should verify the source repository and commit, +name to the workflow matrix. Receivers should verify the source repository and commit, handle duplicate/delayed notifications, and open or update one PR for changed spec content. SDK CI should validate generation from the proposed revision. From 24161387bca1c9a601f414c7f10c7e30d33bbdae Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:15:39 +0800 Subject: [PATCH 5/8] List SDK notification targets on separate lines --- .github/workflows/dispatch-sdk-updates.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml index cd1e31d..aad2251 100644 --- a/.github/workflows/dispatch-sdk-updates.yml +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -15,7 +15,8 @@ jobs: strategy: fail-fast: false matrix: - repository: [braintrust-sdk-rust] + repository: + - braintrust-sdk-rust steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Create scoped notification token From cf03ac332e0335c4ab5a04f4881394f633bdb85e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:16:30 +0800 Subject: [PATCH 6/8] Remove internal SDK automation details from public README --- README.md | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/README.md b/README.md index b53df4b..4911722 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,3 @@ Braintrust's API. JSON and YAML versions of the spec can be found in the See the [Braintrust docs](https://www.braintrustdata.com/docs/api/spec) for documentation of the spec and more resources. - -## SDK update notifications - -Every push to `main` notifies repositories listed in the matrix in -[the dispatch workflow](.github/workflows/dispatch-sdk-updates.yml). Each repository owns -its update workflow and PR structure. Initially only the Rust SDK is listed. -No change in spec content means the receiver has no update to propose. - -The notification is a `repository_dispatch` event named -`braintrust-openapi-updated` with this `client_payload`: - -```json -{ - "spec_sha": "" -} -``` - -Receivers hard-code `braintrustdata/braintrust-openapi` as the source repository; -notifications supply only a commit SHA. - -To add an SDK, merge its receiver on its default branch, then add its repository -name to the workflow matrix. Receivers should verify the source repository and commit, -handle duplicate/delayed notifications, and open or update one PR for changed -spec content. SDK CI should validate generation from the proposed revision. - -The dispatcher uses `repository_dispatch` because the existing Braintrust bot -has Contents write permission; it does not require adding Actions write -permission to the app. `GH_BOT_APP_PRIVATE_KEY` must be available to this repo, -and the app must be installed on each recipient with Contents write access. -The public App ID defaults to `1394756`; override the repository variable -`GH_BOT_APP_ID` if another app is used. Receiver credentials for opening PRs -are configured separately in each SDK repo. - -Install and configure the Rust receiver before merging the dispatcher. Configure -the bot credentials in both repositories before activation. Notifications run -independently per SDK, so a failure does not cancel the other recipients. The -workflow can be run manually on `main` to retry notification of the latest -published revision. No SDK releases or auto-merges are performed. From d5a72860b0bfa5f01756dc737a5f3cd47bfb139d Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:25:35 +0800 Subject: [PATCH 7/8] Send a simple SDK refresh notification --- .github/workflows/dispatch-sdk-updates.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml index aad2251..afc2988 100644 --- a/.github/workflows/dispatch-sdk-updates.yml +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -32,9 +32,7 @@ jobs: env: GH_TOKEN: ${{ steps.bot.outputs.token }} SDK_REPOSITORY: ${{ matrix.repository }} - SPEC_SHA: ${{ github.sha }} run: | set -euo pipefail - jq -n --arg sha "$SPEC_SHA" \ - '{event_type: "braintrust-openapi-updated", client_payload: {spec_sha: $sha}}' | - gh api --method POST "repos/braintrustdata/$SDK_REPOSITORY/dispatches" --input - + gh api --method POST "repos/braintrustdata/$SDK_REPOSITORY/dispatches" \ + -f event_type=braintrust-openapi-updated From 427420733463196f5e776235f7f2fb24461b5dbc Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 30 Oct 2026 21:31:44 +0800 Subject: [PATCH 8/8] Require dedicated credentials for SDK dispatch --- .github/workflows/dispatch-sdk-updates.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dispatch-sdk-updates.yml b/.github/workflows/dispatch-sdk-updates.yml index afc2988..77a0bc2 100644 --- a/.github/workflows/dispatch-sdk-updates.yml +++ b/.github/workflows/dispatch-sdk-updates.yml @@ -23,8 +23,8 @@ jobs: id: bot uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ vars.GH_BOT_APP_ID || '1394756' }} - private-key: ${{ secrets.GH_BOT_APP_PRIVATE_KEY }} + app-id: ${{ vars.SDK_DISPATCH_APP_ID }} + private-key: ${{ secrets.SDK_DISPATCH_APP_PRIVATE_KEY }} owner: braintrustdata repositories: ${{ matrix.repository }} permission-contents: write