Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ inputs:
description: 'Node.js version to install'
required: false
default: '20'
options:
- 20
- 24
- latest
pnpm-version:
description: 'pnpm version to install'
required: false
Expand All @@ -35,10 +31,6 @@ inputs:
required: false
# Default to v4 for backwards compatibility.
default: 'v4'
type: choice
options:
- v4
- v6

runs:
using: 'composite'
Expand Down
40 changes: 34 additions & 6 deletions .github/workflows/build-nodejs-sdk.reusable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ name: BAML SDK Release - Build Node.js
# `@boundaryml/baml-core-node`. Mirrors `build-python-sdk.reusable.yaml` (same
# 8-target set), with `napi-rs` standing in for `maturin`. Each job builds
# one `*.node` native addon and uploads it as `nodejs-sdk-<target>`; the
# publish job in `release-sdk.yaml` downloads them all, runs
# `napi prepublish` to assemble the per-platform npm sub-packages, and
# publishes the umbrella package + sub-packages.
# future npm publisher can download them all, run `napi prepublish` to assemble
# per-platform npm sub-packages, and publish the umbrella package + sub-packages.
# Release builds must pass the frozen BAML language release plan so package.json
# and Rust version surfaces are stamped before any build reads them.

on:
workflow_call: {}
workflow_call:
inputs:
release_plan_json:
description: "Frozen BAML language release-plan.json content"
type: string
default: ""
required: false
source_sha:
description: "Exact repository commit to build"
type: string
default: ""
required: false
push:
branches:
# Mirror the Python reusable workflow's rehearsal-branch trigger so the
Expand Down Expand Up @@ -37,8 +49,9 @@ jobs:
_:
- target: aarch64-unknown-linux-gnu
host: ubuntu-24.04-arm
# Target glibc 2.17. This is the oldest glibc we can practically support.
# Note that ubuntu-22.04-arm has glibc 2.35, but our users need older glibc.
# Target glibc 2.28 for aarch64 using the available rust-cross image.
# This is newer than the ideal glibc floor; our users still need the oldest
# practical compatible baseline we can support for this target.
# See https://github.com/BoundaryML/baml/issues/2736
container: ghcr.io/rust-cross/manylinux_2_28-cross:aarch64
before: |
Expand Down Expand Up @@ -80,6 +93,21 @@ jobs:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.source_sha || github.sha }}
persist-credentials: false

- name: Stamp release plan
env:
RELEASE_PLAN_JSON: ${{ inputs.release_plan_json }}
run: |
set -euo pipefail
if [[ -n "${RELEASE_PLAN_JSON:-}" ]]; then
printf '%s\n' "$RELEASE_PLAN_JSON" > release-plan.json
else
scripts/baml-language-version plan --channel canary --out release-plan.json
fi
scripts/baml-language-version stamp --plan release-plan.json

- name: Check GLIBC version
if: contains(matrix._.target, 'linux') && !contains(matrix._.target, 'musl')
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/build-python-sdk.reusable.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
name: BAML SDK Release - Build Python

on:
workflow_call: {}
workflow_call:
inputs:
release_plan_json:
description: "Frozen BAML language release-plan.json content"
type: string
default: ""
required: false
source_sha:
description: "Exact repository commit to build"
type: string
default: ""
required: false
push:
branches:
- sam/publish-python
Expand Down Expand Up @@ -68,6 +79,21 @@ jobs:
}

- uses: actions/checkout@v6
with:
ref: ${{ inputs.source_sha || github.sha }}
persist-credentials: false

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- name: Stamp release plan
env:
RELEASE_PLAN_JSON: ${{ inputs.release_plan_json }}
run: |
set -euo pipefail
if [[ -n "${RELEASE_PLAN_JSON:-}" ]]; then
printf '%s\n' "$RELEASE_PLAN_JSON" > release-plan.json
else
scripts/baml-language-version plan --channel canary --out release-plan.json
fi
scripts/baml-language-version stamp --plan release-plan.json

# NOTE: we deliberately do NOT use ./.github/actions/setup-mise here.
# The host Python version pins the abi3 floor of the resulting wheel,
Expand Down
41 changes: 38 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,18 @@ jobs:
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':baml_language/**' \
':scripts/baml-language-version' \
':scripts/baml-wrapper-version' \
':scripts/baml-release-manifests' \
':scripts/baml-package-manager-artifacts' \
':scripts/install.sh' \
':scripts/install.ps1' \
':packaging/aur/**' \
':tools/pkg_boundaryml_com/**' \
':.github/workflows/ci.yaml' \
':.github/workflows/release-baml-language-alpha.yml' \
':.github/workflows/release-baml-language.yml' \
':.github/workflows/publish-python-pypi.yml' \
':.github/workflows/build-python-sdk.reusable.yaml' \
':.github/workflows/build-nodejs-sdk.reusable.yaml' \
':.github/workflows/cargo-tests.reusable.yaml' \
':.github/workflows/size-gate.reusable.yaml' \
':.github/workflows/wasm-pack-tests.reusable.yaml' \
Expand Down Expand Up @@ -365,8 +375,33 @@ jobs:
- name: "Validate baml_language release metadata"
run: scripts/baml-language-version check

- name: "Validate alpha version computation"
run: test "$(scripts/baml-language-version compute --alpha-number 1842)" = "$(scripts/baml-language-version show)-alpha.1842"
- name: "Validate canary/nightly version computation"
env:
BAML_LANGUAGE_VERSION_DATE: "20260522"
run: |
set -euo pipefail
canary="$(scripts/baml-language-version show)"
test "$(scripts/baml-language-version compute --channel canary)" = "$canary"
test "$(scripts/baml-language-version compute --channel canary --pypi)" = "$canary"

nightly="$(scripts/baml-language-version compute --channel nightly)"
pypi="$(scripts/baml-language-version compute --channel nightly --pypi)"
python3 - "$canary" "$nightly" "$pypi" <<'PY'
import re
import sys

canary, nightly, pypi = sys.argv[1:]
major, minor, patch = [int(part) for part in canary.split(".")]
base = f"{major}.{minor}.{patch + 1}"
match = re.fullmatch(rf"{re.escape(base)}-nightly\.20260522\.([a-z])", nightly)
if not match:
raise SystemExit(f"unexpected nightly version: {nightly}")

index = ord(match.group(1)) - ord("a")
expected_pypi = f"{base}.dev20260522{index:02d}"
if pypi != expected_pypi:
raise SystemExit(f"unexpected nightly PyPI version: {pypi} != {expected_pypi}")
PY

# Call reusable workflows
cargo-tests:
Expand Down
161 changes: 161 additions & 0 deletions .github/workflows/publish-python-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Publish Python PyPI

# Focused PyPI publisher for Python `baml_core`. The BAML language release
# graph owns version planning/stamping and calls this path after its build gate.
# Before production publishing with this renamed workflow, update or validate
# PyPI's trusted-publisher binding for project `baml-core`.

on:
workflow_call:
inputs:
publish:
description: "Publish to PyPI after building"
type: boolean
default: false
required: false
release_plan_json:
description: "Frozen BAML language release-plan.json content"
type: string
default: ""
required: false
source_sha:
description: "Exact repository commit to build"
type: string
default: ""
required: false
workflow_dispatch:
inputs:
publish:
description: "Publish to PyPI after building (false = build-only rehearsal)"
type: boolean
default: false
required: false

concurrency:
group: publish-python-pypi-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
# Version comes from the stamped checkout prepared by the release graph.
build-python-sdk:
uses: ./.github/workflows/build-python-sdk.reusable.yaml
with:
release_plan_json: ${{ inputs.release_plan_json }}
source_sha: ${{ inputs.source_sha }}

all-builds:
name: Assert all builds passed
runs-on: ubuntu-latest
needs:
- build-python-sdk
steps:
- run: echo "::do-nothing::" >/dev/null

publish-to-pypi:
needs: [all-builds]
if: ${{ inputs.publish }}
# Must use GitHub-hosted runner due to permissions issues with GitHub OIDC trusted publishing
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v8
with:
pattern: python-sdk-*
path: dist
merge-multiple: true
- name: Verify wheel count
run: |
set -euo pipefail
find dist -maxdepth 1 -type f -print
wheel_count=$(find dist -maxdepth 1 -type f -name '*.whl' | wc -l | tr -d ' ')
if [ "$wheel_count" -lt 8 ]; then
echo "::error::Expected at least 8 wheels, found $wheel_count"
exit 1
fi
echo "Found $wheel_count wheels"
- name: Check existing PyPI release
id: pypi
env:
RELEASE_PLAN_JSON: ${{ inputs.release_plan_json }}
run: |
set -euo pipefail
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path

plan_json = os.environ.get("RELEASE_PLAN_JSON", "").strip()
wheels = sorted(Path("dist").glob("baml_core-*.whl"))
if not wheels:
print("::error::No baml_core wheels found in dist", file=sys.stderr)
raise SystemExit(1)

if plan_json:
version = json.loads(plan_json)["pypi_version"]
else:
match = re.match(r"^baml_core-(.+?)-", wheels[0].name)
if not match:
print(f"::error::Could not infer PyPI version from {wheels[0].name}", file=sys.stderr)
raise SystemExit(1)
version = match.group(1)

local_versions = set()
for path in wheels:
match = re.match(r"^baml_core-(.+?)-", path.name)
if not match:
print(f"::error::Could not infer PyPI version from {path.name}", file=sys.stderr)
raise SystemExit(1)
local_versions.add(match.group(1))

if local_versions != {version}:
print(
f"::error::Built wheels do not match PyPI version {version}: {sorted(local_versions)}",
file=sys.stderr,
)
raise SystemExit(1)

local_files = sorted(path.name for path in wheels)
url = f"https://pypi.org/pypi/baml-core/{version}/json"
try:
with urllib.request.urlopen(url, timeout=20) as response:
data = json.load(response)
except urllib.error.HTTPError as exc:
if exc.code == 404:
print(f"pypi_version={version}")
print("release_exists=false")
print("all_files_exist=false")
print(f"PyPI release {version} does not exist yet", file=sys.stderr)
raise SystemExit(0)
raise

remote_files = {item["filename"] for item in data.get("urls", [])}
missing = [name for name in local_files if name not in remote_files]
print(f"pypi_version={version}")
print("release_exists=true")
print(f"all_files_exist={'false' if missing else 'true'}")
if missing:
print(f"PyPI release {version} exists but is missing {len(missing)} file(s); publish will use skip-existing.", file=sys.stderr)
for name in missing:
print(f"missing: {name}", file=sys.stderr)
else:
print(f"PyPI release {version} already contains every built wheel; skipping publish.", file=sys.stderr)
PY
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- name: Publish package to PyPI
if: ${{ steps.pypi.outputs.all_files_exist != 'true' }}
# OIDC trusted publisher binding configured at
# https://pypi.org/manage/project/baml-core/settings/publishing/
# — workflow filename and environment must match this file.
# No password / token in the repo.
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true
Loading
Loading