version and release via changesets (#849)

This commit is contained in:
Adrian Lyjak
2025-10-03 00:08:52 -04:00
committed by GitHub
parent 35ea8476db
commit d028397603
13 changed files with 924 additions and 357 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+6
View File
@@ -0,0 +1,6 @@
---
"llama-cloud-services": patch
"llama-cloud-services-py": patch
---
Update llama-cloud api version, and integrate with agent data deletion
-66
View File
@@ -1,66 +0,0 @@
name: Publish Release - Python
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
UV_VERSION: "0.7.20"
jobs:
build-n-publish:
name: Build and publish to PyPI
if: github.repository == 'run-llama/llama_cloud_services'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install
- name: Display Python version
run: python --version
- name: Build
working-directory: py
run: uv build
- name: Test installing built package
shell: bash
working-directory: py
run: |
uv venv
uv pip install dist/*.whl
- name: Publish package
shell: bash
working-directory: py
run: uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Build and publish llama-parse
working-directory: py/llama_parse/
run: |
uv build
uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }} - LlamaCloud Services PY
artifacts: "py/**/dist/*"
generateReleaseNotes: true
draft: false
prerelease: false
-52
View File
@@ -1,52 +0,0 @@
name: Publish Release - TypeScript
on:
push:
tags:
- "llama-cloud-services@*"
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run Build
working-directory: ts/llama_cloud_services/
run: pnpm build
- name: Build tarball
run: |
pnpm pack
working-directory: ts/llama_cloud_services
- name: Setup npm authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Release
working-directory: ts/llama_cloud_services
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: pnpm publish --access public --no-git-checks
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "ts/llama_cloud_services/llama-cloud-services*.tgz"
name: Release ${{ github.ref_name }} - LlamaCloud Services TS
generateReleaseNotes: true
token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,61 @@
name: Version Bump and Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
# Only run on main branch pushes
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: pnpm install
- name: Add auth token to .npmrc file
run: |
cat << EOF >> ".npmrc"
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Release Pull Request or Publish packages
id: changesets
uses: changesets/action@v1
with:
commit: "chore: version packages"
title: "chore: version packages"
# Custom version script
version: pnpm -w run version
# Custom publish script
publish: pnpm -w run publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
LLAMA_PARSE_PYPI_TOKEN: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
+8 -1
View File
@@ -5,9 +5,16 @@
"private": true,
"keywords": [],
"author": "",
"scripts": {
"pre-commit-version": "pnpm changeset",
"version": "./scripts/changeset-version.py version",
"publish": "./scripts/changeset-version.py publish"
},
"devDependencies": {
"prettier": "^3.6.2",
"lint-staged": "^15.4.2"
"lint-staged": "^15.4.2",
"@changesets/cli": "^2.29.5",
"changesets": "^1.0.2"
},
"lint-staged": {
"ts/llama_cloud_services/src/**/*.{ts,tsx,js,jsx}": [
+575 -10
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,2 +1,3 @@
packages:
- "ts/**"
- "ts/*"
- "py"
+7
View File
@@ -0,0 +1,7 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.55",
"private": "true",
"license": "MIT",
"scripts": {}
}
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "tomlkit", "packaging"]
# ///
"""
This is a script called by the changeset bot. Normally changeset can do the following things, but this is a mixed ts and python repo, so we need to do some extra things.
There's 2 things this does:
- Versioning: Makes changes that may be committed with the newest version.
- Releasing/Tagging: After versions are changed, we check each package to see if its released, and if not, we release it and tag it.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import List
import urllib.request
import urllib.error
import click
import tomlkit
from packaging.version import Version
def _run_command(
cmd: List[str], check: bool = True, capture: bool = True, cwd: Path | None = None
) -> subprocess.CompletedProcess:
"""Run a command and return the result."""
return subprocess.run(
cmd, check=check, capture_output=capture, text=True, cwd=cwd or Path.cwd()
)
def update_python_versions(version: str) -> None:
"""llama-cloud-services and llama-parse share a version. llama-parse is just a silly sidecar that proxies to llama-cloud-services
for compatibility.
This function updates the version in both pyproject.toml files.
"""
# Update main pyproject.toml
main_path = Path("py/pyproject.toml")
main_content = main_path.read_text()
main_doc = tomlkit.parse(main_content)
if main_doc["project"]["version"] != version:
click.echo(f"Updating llama-cloud-services version to {version}")
main_doc["project"]["version"] = version
main_path.write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
parse_path = Path("py/llama_parse/pyproject.toml")
parse_content = parse_path.read_text()
parse_doc = tomlkit.parse(parse_content)
if parse_doc["project"]["version"] != version:
click.echo(f"Updating llama-parse version to {version}")
parse_doc["project"]["version"] = version
parse_path.write_text(tomlkit.dumps(parse_doc))
# Update the dependency reference
dependencies = parse_doc["project"]["dependencies"]
for i, dep in enumerate(dependencies):
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
dependencies[i] = f"llama-cloud-services>={version}"
break
parse_path.write_text(tomlkit.dumps(parse_doc))
click.echo(f"Updated Python packages to version {version}")
def lock_python_dependencies() -> None:
"""Lock Python dependencies."""
try:
_run_command(["uv", "lock"], capture=False)
click.echo("Locked Python dependencies")
except subprocess.CalledProcessError as e:
click.echo(f"Warning: Failed to lock Python dependencies: {e}", err=True)
@click.group()
def cli() -> None:
"""Changeset-based version management for llama-cloud-services."""
pass
@cli.command()
def version() -> None:
"""Apply changeset versions, and propagate them to Python packages."""
# First, run changeset version to update all package.json files (including py/package.json)
_run_command(["npx", "@changesets/cli", "version"], capture=False, check=True)
# Get the updated Python package version from py/package.json (updated by changesets)
py_package_path = Path("py/package.json")
if not py_package_path.exists():
click.echo("Python package.json not found", err=True)
sys.exit(1)
with open(py_package_path) as f:
py_package = json.load(f)
new_version = py_package["version"]
# Update Python pyproject.toml files based on the package.json version
update_python_versions(new_version)
click.echo(f"Successfully propagated version {new_version} to all Python packages")
@cli.command()
@click.option("--tag", is_flag=True, help="Tag the packages after publishing")
@click.option("--dry-run", is_flag=True, help="Dry run the publish")
def publish(tag: bool, dry_run: bool) -> None:
"""Publish all packages."""
# move to the root
os.chdir(Path(__file__).parent.parent)
if not os.getenv("NPM_TOKEN"):
click.echo("NPM_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
if not os.getenv("UV_PUBLISH_TOKEN"):
click.echo("UV_PUBLISH_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
if not os.getenv("LLAMA_PARSE_PYPI_TOKEN"):
click.echo("LLAMA_PARSE_PYPI_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
# not general script. Just checks each of the 2 packages to see if they need to be published.
maybe_publish_ts_package(dry_run)
maybe_publish_py_packages(dry_run)
if tag:
if dry_run:
click.echo("Dry run, skipping tag. Would run:")
click.echo(" npx @changesets/cli tag")
click.echo(" git push --tags")
return
else:
_run_command(["npx", "@changesets/cli", "tag"], check=True, capture=True)
_run_command(["git", "push", "--tags"], check=True, capture=True)
def maybe_publish_ts_package(dry_run: bool) -> None:
"""Publish the ts package if it needs to be published."""
target_dir = Path("ts/llama_cloud_services")
ts_path_package = target_dir / "package.json"
package_json = json.loads(ts_path_package.read_text())
version = package_json["version"]
# Check if this version is already published on npm
result = _run_command(
["npm", "view", "llama-cloud-services", "versions", "--json"],
check=True,
capture=True,
cwd=target_dir,
)
published_versions = json.loads(result.stdout)
if version in published_versions:
click.echo(
f"npm package llama-cloud-services@{version} already published, skipping"
)
return
click.echo(f"Publishing llama-cloud-services@{version}")
# defer to the package.json publish script
if dry_run:
click.echo("Dry run, skipping publish. Would run:")
click.echo(" pnpm run publish")
return
else:
output = _run_command(
["pnpm", "runpublish"], check=True, capture=True, cwd=target_dir
)
click.echo(output.stdout)
def maybe_publish_py_packages(dry_run: bool) -> None:
"""Publish the py packages if they need to be published."""
for pyproject in list(Path("py").glob("*/pyproject.toml")) + [
Path("py/pyproject.toml")
]:
name, version = current_version(pyproject)
if is_published(name, version):
click.echo(f"PyPI package {name}@{version} already published, skipping")
continue
click.echo(f"Publishing {name}@{version}")
# Use different tokens for different packages
env = os.environ.copy()
if name == "llama-parse":
# llama-parse uses its own token
env["UV_PUBLISH_TOKEN"] = os.environ["LLAMA_PARSE_PYPI_TOKEN"]
else:
# llama-cloud-services uses the main PyPI token
env["UV_PUBLISH_TOKEN"] = os.environ["UV_PUBLISH_TOKEN"]
if dry_run:
token = env["UV_PUBLISH_TOKEN"]
summary = (token[:3] + "***") if len(token) <= 6 else token[:6] + "****"
click.echo(
f"Dry run, skipping publish. Would run with publish token {summary}:"
)
click.echo(" uv publish --dry-run")
return
else:
result = subprocess.run(
["uv", "publish"],
check=True,
capture_output=True,
text=True,
cwd=pyproject.parent,
env=env,
)
click.echo(result.stdout)
def current_version(pyproject: Path) -> tuple[str, str]:
"""Return (package_name, version_str) taken from the given pyproject.toml."""
doc = tomlkit.parse(pyproject.read_text())
name = doc["project"]["name"]
version = str(Version(doc["project"]["version"])) # normalise
return name, version
def is_published(
name: str, version: str, index_url: str = "https://pypi.org/pypi"
) -> bool:
"""
True → `<name>==<version>` exists on the given index
False → package missing *or* version missing
"""
url = f"{index_url.rstrip('/')}/{name}/json"
try:
data = json.load(urllib.request.urlopen(url))
except urllib.error.HTTPError as e: # 404 → package not published at all
if e.code == 404:
return False
raise # any other error should surface
return version in data["releases"] # keys are version strings
if __name__ == "__main__":
cli()
-226
View File
@@ -1,226 +0,0 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "tomlkit"]
# ///
import click
import subprocess
import sys
import tomlkit
from pathlib import Path
import json
def get_current_versions() -> tuple[str, str, str, str | None]:
"""Get current versions from both pyproject.toml files and TS package.json."""
# Read main pyproject.toml
main_content = Path("py/pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_version = main_doc["project"]["version"]
# Read llama_parse/pyproject.toml
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_version = llama_parse_doc["project"]["version"]
# Find llama-cloud-services dependency in the dependencies list
dependency_version = None
for dep in llama_parse_doc["project"]["dependencies"]:
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
dependency_version = (
dep.split("==")[1]
if "==" in dep
else dep.split(">=")[1]
if ">=" in dep
else None
)
break
# Read TypeScript package.json version via helper
ts_version: str = get_ts_version()
return (
str(main_version),
str(llama_parse_version),
str(dependency_version),
str(ts_version) if ts_version is not None else None,
)
def validate_versions(
main_version: str,
llama_parse_version: str,
dependency_version: str,
) -> list[str]:
"""Validate that versions are consistent and return warnings."""
warnings = []
if main_version != llama_parse_version:
warnings.append(
f"Version mismatch: main={main_version}, llama_parse={llama_parse_version}"
)
# Extract version from dependency string (e.g., ">=0.6.51" -> "0.6.51")
if dependency_version and dependency_version.startswith(">="):
dep_ver = dependency_version[2:]
if dep_ver != main_version:
warnings.append(
f"Dependency version mismatch: dependency={dep_ver}, main={main_version}"
)
return warnings
def set_version(version: str) -> None:
"""Set version across Python projects (no TS change)."""
# Update main pyproject.toml
main_content = Path("py/pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_doc["project"]["version"] = version
Path("py/pyproject.toml").write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_doc["project"]["version"] = version
for dep_index, dep in enumerate(llama_parse_doc["project"]["dependencies"]):
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
llama_parse_doc["project"]["dependencies"][
dep_index
] = f"llama-cloud-services>={version}"
break
Path("py/llama_parse/pyproject.toml").write_text(tomlkit.dumps(llama_parse_doc))
click.echo(f"Updated Python versions to {version}")
def get_ts_version() -> str:
"""Read TypeScript package.json version (if present)."""
ts_package_path = Path("ts/llama_cloud_services/package.json")
package_data = json.loads(ts_package_path.read_text())
data = package_data.get("version")
if data is None:
raise RuntimeError("TypeScript package.json version not found")
return data
def set_ts_version(version: str) -> None:
"""Set TypeScript package.json version only."""
ts_package_path = Path("ts/llama_cloud_services/package.json")
package_data = json.loads(ts_package_path.read_text())
package_data["version"] = version
ts_package_path.write_text(json.dumps(package_data, indent=2) + "\n")
click.echo(f"Updated TypeScript package.json version to {version}")
def get_current_branch() -> str:
"""Get the current git branch."""
result = subprocess.run(
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
)
return result.stdout.strip()
def create_if_not_exists(version: str) -> str:
"""Create a git tag and push it."""
current_branch = get_current_branch()
if current_branch != "main":
click.echo(
f"Error: Not on main branch (currently on {current_branch})", err=True
)
sys.exit(1)
tag_name = f"v{version}" if version[0].isdigit() else version
if not tag_exists(tag_name):
# Create tag
subprocess.run(["git", "tag", tag_name], check=True)
click.echo(f"Created tag {tag_name}")
else:
click.echo(f"Tag {tag_name} already exists")
return tag_name
def tag_exists(tag_name: str) -> bool:
"""Check if a git tag exists."""
result = subprocess.run(
["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True
)
return tag_name in result.stdout.strip()
def push_tag(tag_name: str) -> None:
"""Push a git tag."""
subprocess.run(["git", "push", "origin", tag_name], check=True)
click.echo(f"Pushed tag {tag_name}")
@click.group()
def cli() -> None:
"""Version management for llama-cloud-services."""
pass
@cli.command()
def get() -> None:
"""Get current versions and show validation warnings."""
(
main_version,
llama_parse_version,
dependency_version,
ts_version,
) = get_current_versions()
click.echo("Current versions:")
click.echo(f" llama-cloud-services: {main_version}")
click.echo(f" llama-parse: {llama_parse_version}")
click.echo(f" dependency reference: {dependency_version}")
click.echo(f" typescript package: {ts_version}")
warnings = validate_versions(main_version, llama_parse_version, dependency_version)
if warnings:
click.echo("\nValidation warnings:")
for warning in warnings:
click.echo(f" ⚠️ {warning}")
else:
click.echo("\n✅ All versions are consistent")
@cli.command()
@click.argument("version")
@click.option("--js", is_flag=True, help="Update TypeScript package.json only")
def set(version: str, js: bool) -> None:
"""Set version for Python, TypeScript, or both (default: Python only)."""
if js:
set_ts_version(version)
return
else:
set_version(version)
@cli.command()
@click.option(
"--version", help="Version to tag (uses current version if not specified)"
)
@click.option(
"--push",
is_flag=True,
help="Push the tag to the remote repository",
)
@click.option(
"--js",
is_flag=True,
help="tag TypeScript package.json only",
)
def tag(version: str | None = None, push: bool = False, js: bool = False) -> None:
"""Create and push a git tag for the current version."""
if not version:
main_version, _, _, js_version = get_current_versions()
version = f"llama-cloud-services@{js_version}" if js else main_version
tag_name = create_if_not_exists(version)
if push:
push_tag(tag_name)
if __name__ == "__main__":
cli()
+2 -1
View File
@@ -13,7 +13,8 @@
"test": "vitest run --testTimeout=60000",
"test:watch": "vitest --watch",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage",
"release": "pnpm run build && pnpm publish"
},
"files": [
"openapi.json",