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
31 changes: 31 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: release-please

# Opens/maintains one "Release PR" per runtime and, when a Release PR is merged,
# tags the runtime (e.g. python/2.0.0). Existing per-runtime publish workflows
# (prompty-python.yml, prompty-rust.yml, prompty-ts-release.yml, prompty-csharp.yml)
# trigger on those tags and do the actual package publishing.
#
# IMPORTANT: `token` must be a GitHub App installation token or a PAT — NOT the
# default GITHUB_TOKEN. Tags created with GITHUB_TOKEN do not trigger the
# tag-listening publish workflows (GitHub's recursive-workflow guard). Provide a
# token via the RELEASE_PLEASE_TOKEN secret so the publish workflows fire.

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
9 changes: 9 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"runtime/python/prompty": "2.0.0-beta.3",
"runtime/rust": "2.0.0-beta.4",
"runtime/typescript/packages/core": "2.0.0-beta.5",
"runtime/csharp": "2.0.0-beta.4",
"runtime/java": "2.0.0-beta.4",
"runtime/go/prompty": "2.0.0-beta.4",
"runtime/swift/prompty": "2.0.0-beta.4"
}
75 changes: 75 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,81 @@ via OIDC (no secrets needed).
| Python | [PyPI](https://pypi.org/project/prompty/) | `python/{version}` | `prompty-python.yml` |
| TypeScript | [npm](https://www.npmjs.com/package/@prompty/core) | `typescript/{version}` | `prompty-ts-release.yml` |

Those tags are now produced automatically by **release-please** (see below). The
manual tag-and-push steps later in this document remain as a break-glass path.

## Automated releases (release-please)

Prompty ships **seven runtimes independently** via
[release-please](https://github.com/googleapis/release-please). There is no shared
version number — `python/2.1.0` and `rust/2.0.3` can coexist. Config lives in
`release-please-config.json` + `.release-please-manifest.json` at the repo root, and
the driver is `.github/workflows/release-please.yml`.

> **VS Code extension** (`vscode/`) is intentionally **not** managed by
> release-please — it uses the Marketplace pre-release channel (`vscode/2.0.0-pre.N`)
> and keeps its own pipeline.

### Flow

```
conventional commit on main
│
▼
release-please.yml ── opens/updates one "Release PR" per runtime with changes
│ (title: "chore(<runtime>): release <ver>")
▼
merge the Release PR ── bumps version files, writes CHANGELOG, pushes a tag
│ like `python/2.1.0`
▼
prompty-<runtime>.yml ── existing publish workflow triggers on the tag and publishes
```

release-please **only opens PRs and cuts tags**; nothing publishes until a human
merges a Release PR. Commits route to a runtime by the **paths they touch**; add a
scope when ambiguous (`feat(python):`, `fix(rust):`). Only `feat`, `fix`, `perf`,
`revert`, `deps` trigger releases; `feat!:` / `BREAKING CHANGE:` bumps major.

### Components

| Component | Path | release-type | Version source | Publish |
| ---------- | ---------------------------------- | ------------ | ---------------------------------------------------- | ------- |
| `python` | `runtime/python/prompty` | `python` | `prompty/_version.py` (`# x-release-please-version`) | ✅ PyPI |
| `rust` | `runtime/rust` | `rust` | all workspace `Cargo.toml` (native) | ✅ crates|
| `typescript` | `runtime/typescript/packages/core` | `node` | all `packages/*/package.json` (`node-workspace`) | ✅ npm |
| `csharp` | `runtime/csharp` | `simple` | 4 `*.csproj` `<Version>` (annotated) | ✅ NuGet |
| `java` | `runtime/java` | `simple` | `build.gradle.kts` `version` (annotated) | check only |
| `go` | `runtime/go/prompty` | `go` | git tag only | check only |
| `swift` | `runtime/swift/prompty` | `simple` | git tag only | check only |

Tag format (`separator:"/"`, `include-component-in-tag:true`, `include-v-in-tag:false`)
reproduces the existing `python/2.1.0` tags exactly — **no publish workflow changed**.

### Graduating beta → 2.0.0

All runtimes were on `2.0.0-beta.N`. The manifest is seeded at each runtime's last
beta with **no prerelease mode**, so the first Release PR graduates each runtime to a
clean **`2.0.0`** stable release; normal `2.0.1` / `2.1.0` / `3.0.0` bumps follow.

### One-time setup before enabling on `main`

1. **`RELEASE_PLEASE_TOKEN` secret (required for auto-publish).** The workflow falls
back to `GITHUB_TOKEN`, but tags cut with `GITHUB_TOKEN` **do not trigger** the
tag-listening publish workflows. Provide a **GitHub App installation token** (or
fine-grained PAT) with `contents:write` + `pull_requests:write` as
`RELEASE_PLEASE_TOKEN`.
2. **`bootstrap-sha`** in the config is pinned to the adoption commit so the first run
doesn't replay old history.

### Verify on the first Release PR (before merging)

- **typescript** — confirm all four `packages/*/package.json` versions **and** their
`^@prompty/core` ranges bump (`node-workspace`); if a sibling is missed, register it
as its own package in the config.
- **rust** — confirm all workspace members + `path`/`version` deps bump together.
- **csharp / java** — confirm the annotated `<Version>` / `version =` lines rewrite.
- **python** — Release PR sets `_version.py` to `2.0.0`; the built wheel is `prompty-2.0.0`.

## Pre-flight checklist (MUST DO before every release)

Run these **exact** steps locally before pushing tags. They mirror CI.
Expand Down
66 changes: 66 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"separator": "/",
"include-component-in-tag": true,
"include-v-in-tag": false,
"bootstrap-sha": "3c1c0cc27ee429983ee362aa60ac510678da4af9",
"changelog-sections": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "revert", "section": "Reverts" },
{ "type": "deps", "section": "Dependencies" },
{ "type": "docs", "section": "Documentation", "hidden": true },
{ "type": "chore", "section": "Miscellaneous", "hidden": true },
{ "type": "ci", "section": "Continuous Integration", "hidden": true },
{ "type": "test", "section": "Tests", "hidden": true },
{ "type": "refactor", "section": "Code Refactoring", "hidden": true }
],
"packages": {
"runtime/python/prompty": {
"component": "python",
"release-type": "python",
"package-name": "prompty",
"extra-files": [
{ "type": "generic", "path": "prompty/_version.py" }
]
},
"runtime/rust": {
"component": "rust",
"release-type": "rust"
},
"runtime/typescript/packages/core": {
"component": "typescript",
"release-type": "node",
"package-name": "@prompty/core"
},
"runtime/csharp": {
"component": "csharp",
"release-type": "simple",
"extra-files": [
{ "type": "generic", "path": "Prompty.Core/Prompty.Core.csproj" },
{ "type": "generic", "path": "Prompty.OpenAI/Prompty.OpenAI.csproj" },
{ "type": "generic", "path": "Prompty.Foundry/Prompty.Foundry.csproj" },
{ "type": "generic", "path": "Prompty.Anthropic/Prompty.Anthropic.csproj" }
]
},
"runtime/java": {
"component": "java",
"release-type": "simple",
"extra-files": [
{ "type": "generic", "path": "build.gradle.kts" }
]
},
"runtime/go/prompty": {
"component": "go",
"release-type": "go"
},
"runtime/swift/prompty": {
"component": "swift",
"release-type": "simple"
}
},
"plugins": [
"node-workspace"
]
}
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>2.0.0-beta.4</Version>
<Version>2.0.0-beta.4</Version> <!-- x-release-please-version -->
<PackageId>Prompty.Anthropic</PackageId>
<Authors>Microsoft</Authors>
<Description>Anthropic provider for Prompty — executor and processor for Claude models via the Anthropic Messages API.</Description>
Expand Down
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Core/Prompty.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>2.0.0-beta.4</Version>
<Version>2.0.0-beta.4</Version> <!-- x-release-please-version -->
<PackageId>Prompty.Core</PackageId>
<Authors>Microsoft</Authors>
<Description>Prompty is an asset class and format for LLM prompts. Core library with loader, pipeline, renderers, parsers, and tracing.</Description>
Expand Down
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<NoWarn>OPENAI001;CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>2.0.0-beta.4</Version>
<Version>2.0.0-beta.4</Version> <!-- x-release-please-version -->
<PackageId>Prompty.Foundry</PackageId>
<Authors>Microsoft</Authors>
<Description>Microsoft Foundry (Azure OpenAI) provider for Prompty — executor and processor for Azure-hosted models.</Description>
Expand Down
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<NoWarn>OPENAI001;CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>2.0.0-beta.4</Version>
<Version>2.0.0-beta.4</Version> <!-- x-release-please-version -->
<PackageId>Prompty.OpenAI</PackageId>
<Authors>Microsoft</Authors>
<Description>OpenAI provider for Prompty — executor and processor for OpenAI chat, embedding, image, and agent APIs.</Description>
Expand Down
2 changes: 1 addition & 1 deletion runtime/java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {

allprojects {
group = "com.microsoft.prompty"
version = "2.0.0-beta.4"
version = "2.0.0-beta.4" // x-release-please-version
}

subprojects {
Expand Down
2 changes: 1 addition & 1 deletion runtime/python/prompty/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ OPENAI_API_KEY=
OPENAI_BASE_URL=
OPENAI_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
OPENAI_IMAGE_MODEL=dall-e-2
OPENAI_IMAGE_MODEL=gpt-image-1

# Direct OpenAI (api.openai.com — no proxy/compat layer)
DIRECT_OPENAI_API_KEY=
Expand Down
4 changes: 0 additions & 4 deletions runtime/python/prompty/prompty/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@
"bind_tools",
"tool",
# Backward-compat aliases
"AzureExecutor",
"AzureProcessor",
"PromptAgent",
"AgentDefinition",
# Model discovery
Expand Down Expand Up @@ -280,7 +278,5 @@
)

# Backward-compat aliases (will be removed in a future version)
AzureExecutor = FoundryExecutor
AzureProcessor = FoundryProcessor
PromptAgent = Agent
AgentDefinition = Agent
2 changes: 1 addition & 1 deletion runtime/python/prompty/prompty/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "2.0.0b3"
VERSION = "2.0.0-beta.3" # x-release-please-version
6 changes: 3 additions & 3 deletions runtime/python/prompty/prompty/core/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ def _discover(group: str, key: str) -> Any:
return _cache[cache_key]

eps = importlib.metadata.entry_points(group=group, name=key)
ep_list = list(eps) if not isinstance(eps, list) else eps
if not ep_list:
first = next(iter(eps), None)
if first is None:
raise InvokerError(group, key)

loaded = _instantiate(ep_list[0].load())
loaded = _instantiate(first.load())
with _lock:
_cache[cache_key] = loaded
return loaded
Expand Down
9 changes: 0 additions & 9 deletions runtime/python/prompty/prompty/providers/azure/__init__.py

This file was deleted.

7 changes: 0 additions & 7 deletions runtime/python/prompty/prompty/providers/azure/executor.py

This file was deleted.

7 changes: 0 additions & 7 deletions runtime/python/prompty/prompty/providers/azure/processor.py

This file was deleted.

2 changes: 1 addition & 1 deletion runtime/python/prompty/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ ignore = ["E501"]
[tool.pytest.ini_options]
asyncio_mode = "strict"
markers = [
"integration: tests that hit real OpenAI / Azure OpenAI endpoints (deselected by default)",
"integration: tests that hit real OpenAI / Azure OpenAI (Foundry) endpoints (deselected by default)",
]
addopts = "-m 'not integration'"

Expand Down
62 changes: 61 additions & 1 deletion runtime/python/prompty/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

from __future__ import annotations

import json
import os
from functools import cache
from pathlib import Path
from typing import Any

import pytest
Expand Down Expand Up @@ -46,7 +49,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
_OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "") # optional: proxy via Azure
_OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") # override default chat model
_OPENAI_EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
_OPENAI_IMAGE_MODEL = os.environ.get("OPENAI_IMAGE_MODEL", "dall-e-2")
_OPENAI_IMAGE_MODEL = os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-1")
Comment thread
sethjuarez marked this conversation as resolved.
_AZURE_KEY = os.environ.get("AZURE_OPENAI_API_KEY", "")
_AZURE_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT", "")
_AZURE_CHAT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT", "")
Expand Down Expand Up @@ -405,3 +408,60 @@ def make_entra_agent(
if metadata is not None:
data["metadata"] = metadata
return Agent.load(data)


# ---------------------------------------------------------------------------
# Conformance vector access
# ---------------------------------------------------------------------------
#
# Live agent-loop tests are anchored to the SAME canonical @vector corpus the
# deterministic model-conformance suite replays (schema/tsp-output/
# .typra-generated/vectors.json, emitted by Typra from the TypeSpec sources).
# The live test reuses a vector's scenario — its messages, tools, and derived
# tool-dispatch order — but swaps the scripted model double for a real provider,
# so one Typra-governed definition validates both the deterministic engine and
# the live provider path.

# repo root: tests/integration -> tests -> prompty -> python -> runtime -> root
_VECTORS_JSON = Path(__file__).resolve().parents[5] / "schema" / "tsp-output" / ".typra-generated" / "vectors.json"


@cache
def _load_vectors_corpus() -> tuple[dict[str, Any], ...]:
if not _VECTORS_JSON.exists():
raise FileNotFoundError(
f"Conformance vector corpus not found at {_VECTORS_JSON}. "
"Regenerate it via the Typra emitter (schema build)."
)
with open(_VECTORS_JSON, encoding="utf-8") as handle:
return tuple(json.load(handle).get("vectors", []))


def load_turn_vector(name: str) -> dict[str, Any]:
"""Return the ``TurnConformance.run`` vector named ``name`` from the corpus.

Raises ``KeyError`` if no such vector exists so a renamed or removed vector
fails loudly rather than silently skipping live coverage.
"""
for entry in _load_vectors_corpus():
if (
entry.get("contract") == "TurnConformance"
and entry.get("operation") == "run"
and isinstance(entry.get("vector"), dict)
and entry["vector"].get("name") == name
):
return entry["vector"]
raise KeyError(f"TurnConformance.run vector '{name}' not found in {_VECTORS_JSON}")


def turn_vector_tool_order(vector: dict[str, Any]) -> list[str]:
"""Derive the expected ordered tool-dispatch sequence from a run vector.

Reads each scripted turn's ``expected_tool_calls`` in order — this is the
dependency chain the live provider must reproduce.
"""
order: list[str] = []
for step in vector.get("sequence", []):
for call in step.get("expected_tool_calls") or []:
order.append(call["name"])
return order
Loading
Loading