diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..48701ce4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: "Release" + +on: + push: + tags: + - "*" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + build: + name: Build and Publish + runs-on: ubuntu-latest + environment: + # Create this environment in the GitHub repository under Settings -> Environments + name: release + permissions: + actions: read + contents: read + id-token: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: "3.12" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/0.5.24/install.sh -o install.sh + echo "f476e445f4a56234fcc12ed478289f80e8e97b230622d8ce2f2406ebfeeb2620 install.sh" > checksum.txt + sha256sum --check checksum.txt + chmod +x install.sh + ./install.sh + uv --version + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + uv sync --frozen --all-packages + + - name: Build packages + run: | + uv build --all-packages + + - name: Publish to PyPI + run: | + uv publish --trusted-publishing always \ No newline at end of file diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..5a2f9516 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +# Release steps + +1. Run `uv run scripts/release.py `. See [Bump types](#bump-types) for available options. +2. This should bump all the versions for the packages and also created a release branch. +3. Create a PR and get it merged. +4. Now go to https://github.com/microsoft/teams.py/releases/new and create a new release. +5. This will automatically kick off a release workflow that needs to be aproved. +6. Once approved, the release will be published to PyPI. + +## Appendix + +# Bump types +Version bump types: + major - Increment major version (1.0.0 -> 2.0.0) + minor - Increment minor version (1.0.0 -> 1.1.0) + patch - Increment patch version (1.0.0 -> 1.0.1) + stable - Remove pre-release suffix (1.0.0a1 -> 1.0.0) + alpha - Add/increment alpha pre-release (1.0.0 -> 1.0.0a1) + beta - Add/increment beta pre-release (1.0.0 -> 1.0.0b1) + rc - Add/increment release candidate (1.0.0 -> 1.0.0rc1) + post - Add/increment post-release (1.0.0 -> 1.0.0.post1) + dev - Add/increment dev release (1.0.0 -> 1.0.0.dev1) + \ No newline at end of file diff --git a/packages/api/pyproject.toml b/packages/api/pyproject.toml index 5a6f17d9..67bb4bb5 100644 --- a/packages/api/pyproject.toml +++ b/packages/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-api" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "API package for Microsoft Teams" readme = "README.md" repository = "https://github.com/microsoft/teams.py" diff --git a/packages/apps/pyproject.toml b/packages/apps/pyproject.toml index ad7461ad..0c57909a 100644 --- a/packages/apps/pyproject.toml +++ b/packages/apps/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-apps" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "The app package for a Microsoft Teams agent" authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] readme = "README.md" diff --git a/packages/cards/pyproject.toml b/packages/cards/pyproject.toml index e3a20dd8..d8fdb066 100644 --- a/packages/cards/pyproject.toml +++ b/packages/cards/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-cards" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "Cards package for Microsoft Teams" readme = "README.md" repository = "https://github.com/microsoft/teams.py" diff --git a/packages/common/pyproject.toml b/packages/common/pyproject.toml index 894444b0..fefc9974 100644 --- a/packages/common/pyproject.toml +++ b/packages/common/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-common" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "Common package for Microsoft Teams" readme = "README.md" repository = "https://github.com/microsoft/teams.py" diff --git a/packages/devtools/pyproject.toml b/packages/devtools/pyproject.toml index 8886641b..d91fd6ab 100644 --- a/packages/devtools/pyproject.toml +++ b/packages/devtools/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-devtools" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "Local tool to streamline testing and development" authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] readme = "README.md" @@ -16,6 +16,10 @@ dependencies = [ "pydantic>=2.0.0", ] +[tool.uv.sources] +"microsoft-teams-api" = { workspace = true } +"microsoft-teams-apps" = { workspace = true } + [project.urls] Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/devtools/src/microsoft/teams/devtools" diff --git a/packages/graph/pyproject.toml b/packages/graph/pyproject.toml index dbde5b99..33767681 100644 --- a/packages/graph/pyproject.toml +++ b/packages/graph/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-graph" -version = "0.0.1-alpha.1" +version = "0.0.1a2" description = "The Graph package for a Microsoft Teams agent" readme = "README.md" license = { text = "MIT" } @@ -36,4 +36,4 @@ packages = ["src/microsoft"] include = ["src"] [tool.uv.sources] -microsoft-teams-common = { workspace = true } \ No newline at end of file +microsoft-teams-common = { workspace = true } diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 00000000..c2dd6942 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import argparse +import subprocess +import sys +from pathlib import Path +from typing import Dict, List + +import tomllib + + +def get_packages_dir() -> Path: + """Get the packages directory relative to the script location.""" + script_dir = Path(__file__).parent + return script_dir.parent / "packages" + + +def find_packages() -> List[Path]: + """Find all package directories containing pyproject.toml.""" + packages_dir = get_packages_dir() + packages: List[Path] = [] + + for item in packages_dir.iterdir(): + if item.is_dir() and (item / "pyproject.toml").exists(): + packages.append(item) + + return sorted(packages) + + +def dry_run_version_bump(package_path: Path, bump_type: str) -> str: + """Run a dry-run version bump to see what the new version would be.""" + try: + result = subprocess.run( + ["uv", "version", "--bump", bump_type, "--dry-run"], + cwd=package_path, + capture_output=True, + text=True, + check=True, + ) + # Extract the version from the output + # Handle multiple formats: + # Format 1: "Would bump version from X.Y.Z to A.B.C" + # Format 2: "package-name X.Y.Z => A.B.C" + # Format 3: Just "A.B.C" + output = result.stdout.strip() + + if " to " in output: + return output.split(" to ")[-1] + elif " => " in output: + return output.split(" => ")[-1] + else: + # Fallback: extract version from the end of the output + return output.split()[-1] + except subprocess.CalledProcessError as e: + print(f" ✗ Failed to dry-run bump {package_path.name}: {e.stderr}") + sys.exit(1) + + +def bump_package_version(package_path: Path, bump_type: str, verbose: bool = False) -> str: + """Bump the version of a package and return the new version.""" + print(f"Bumping {package_path.name} version ({bump_type})...") + + try: + result = subprocess.run( + ["uv", "version", "--bump", bump_type], + cwd=package_path, + capture_output=not verbose, + text=True, + check=True, + ) + print(f" ✓ {package_path.name}: {result.stdout.strip()}") + return get_package_version(package_path) + except subprocess.CalledProcessError as e: + print(f" ✗ Failed to bump {package_path.name}: {e.stderr}") + sys.exit(1) + + +def get_package_version(package_path: Path) -> str: + """Extract version from pyproject.toml.""" + pyproject_path = package_path / "pyproject.toml" + + try: + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + return data["project"]["version"] + except (KeyError, tomllib.TOMLDecodeError, OSError) as e: + print(f"Error reading version from {pyproject_path}: {e}") + sys.exit(1) + + +def create_release_branch(version: str, verbose: bool = False) -> str: + """Create a new release branch.""" + branch_name = f"release_{version}" + + try: + # Create and switch to new branch + subprocess.run(["git", "checkout", "-b", branch_name], check=True, capture_output=not verbose) + print(f"Created and switched to branch: {branch_name}") + + # Add all changes + subprocess.run(["git", "add", "."], check=True, capture_output=not verbose) + + # Commit changes + subprocess.run(["git", "commit", "-m", f"Release version {version}"], check=True, capture_output=not verbose) + print(f"Committed changes for release {version}") + + return branch_name + except subprocess.CalledProcessError as e: + print(f"Error creating release branch: {e}") + sys.exit(1) + + +def main() -> None: + """Main script entry point.""" + parser = argparse.ArgumentParser( + description="Release script for Microsoft Teams Python SDK", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Version bump types: + major - Increment major version (1.0.0 -> 2.0.0) + minor - Increment minor version (1.0.0 -> 1.1.0) + patch - Increment patch version (1.0.0 -> 1.0.1) + stable - Remove pre-release suffix (1.0.0a1 -> 1.0.0) + alpha - Add/increment alpha pre-release (1.0.0 -> 1.0.0a1) + beta - Add/increment beta pre-release (1.0.0 -> 1.0.0b1) + rc - Add/increment release candidate (1.0.0 -> 1.0.0rc1) + post - Add/increment post-release (1.0.0 -> 1.0.0.post1) + dev - Add/increment dev release (1.0.0 -> 1.0.0.dev1) + """, + ) + + parser.add_argument( + "bump_type", + choices=["major", "minor", "patch", "stable", "alpha", "beta", "rc", "post", "dev"], + help="Type of version bump to perform", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Show detailed output from commands", + ) + + args = parser.parse_args() + + # Find all packages + packages = find_packages() + if not packages: + print("No packages found in packages/ directory") + sys.exit(1) + + print(f"Found {len(packages)} packages:") + for pkg in packages: + print(f" - {pkg.name}") + print() + + # First, do a dry-run to check all packages would have the same version + print("Running dry-run to check version consistency...") + dry_run_versions: Dict[str, str] = {} + for package in packages: + new_version = dry_run_version_bump(package, args.bump_type) + dry_run_versions[package.name] = new_version + print(f" {package.name}: {get_package_version(package)} -> {new_version}") + + # Check if all packages would have the same version + unique_dry_run_versions = set(dry_run_versions.values()) + if len(unique_dry_run_versions) != 1: + print("\n❌ ERROR: Packages would have different versions after bump:") + for pkg, ver in dry_run_versions.items(): + print(f" {pkg}: {ver}") + print("\nAll packages must have the same version. Please fix version inconsistencies first.") + sys.exit(1) + + target_version = next(iter(unique_dry_run_versions)) + print(f"\n✓ All packages will be bumped to: {target_version}") + print("\nProceeding with actual version bump...") + + # Now do the actual version bump + versions: Dict[str, str] = {} + for package in packages: + new_version = bump_package_version(package, args.bump_type, args.verbose) + versions[package.name] = new_version + + # Verify all packages have the same version (should always pass now) + unique_versions = set(versions.values()) + if len(unique_versions) != 1: + print("❌ CRITICAL ERROR: Packages have different versions after bump (this should not happen):") + for pkg, ver in versions.items(): + print(f" {pkg}: {ver}") + sys.exit(1) + + # Use the first version as the release version + release_version = next(iter(unique_versions)) + print(f"\nAll packages bumped to version: {release_version}") + + # Ask user about creating branch + response = input("\nWould you like to create a release branch (y/N): ").strip().lower() + + if response in ("y", "yes"): + branch_name = create_release_branch(release_version, args.verbose) + print(f"\n✓ Release {release_version} is ready!") + print(f" Branch: {branch_name}") + else: + print(f"\nVersion bump complete. Release version: {release_version}") + print("You can manually commit and create a branch/PR when ready.") + + +if __name__ == "__main__": + main() diff --git a/tests/echo/pyproject.toml b/tests/echo/pyproject.toml index 23816d0d..b8be693b 100644 --- a/tests/echo/pyproject.toml +++ b/tests/echo/pyproject.toml @@ -7,6 +7,8 @@ requires-python = ">=3.12" dependencies = [ "dotenv>=0.9.9", "microsoft-teams-apps", + "microsoft-teams-api", + "microsoft-teams-devtools" ] [tool.uv.sources] diff --git a/uv.lock b/uv.lock index 4919ca80..11d26903 100644 --- a/uv.lock +++ b/uv.lock @@ -483,13 +483,17 @@ version = "0.1.0" source = { virtual = "tests/echo" } dependencies = [ { name = "dotenv" }, + { name = "microsoft-teams-api" }, { name = "microsoft-teams-apps" }, + { name = "microsoft-teams-devtools" }, ] [package.metadata] requires-dist = [ { name = "dotenv", specifier = ">=0.9.9" }, + { name = "microsoft-teams-api", editable = "packages/api" }, { name = "microsoft-teams-apps", editable = "packages/apps" }, + { name = "microsoft-teams-devtools", editable = "packages/devtools" }, ] [[package]] @@ -905,7 +909,7 @@ wheels = [ [[package]] name = "microsoft-teams-api" -version = "0.0.1a1" +version = "0.0.1a3" source = { editable = "packages/api" } dependencies = [ { name = "microsoft-teams-cards" }, @@ -938,7 +942,7 @@ dev = [ [[package]] name = "microsoft-teams-apps" -version = "0.0.1a1" +version = "0.0.1a2" source = { editable = "packages/apps" } dependencies = [ { name = "cryptography" }, @@ -977,7 +981,7 @@ test = [ [[package]] name = "microsoft-teams-cards" -version = "0.0.1a1" +version = "0.0.1a2" source = { editable = "packages/cards" } dependencies = [ { name = "coverage" }, @@ -996,7 +1000,7 @@ requires-dist = [ [[package]] name = "microsoft-teams-common" -version = "0.0.1a1" +version = "0.0.1a2" source = { editable = "packages/common" } dependencies = [ { name = "coverage" }, @@ -1027,7 +1031,7 @@ dev = [ [[package]] name = "microsoft-teams-devtools" -version = "0.0.1a1" +version = "0.0.1a2" source = { editable = "packages/devtools" } dependencies = [ { name = "fastapi" }, @@ -1048,7 +1052,7 @@ requires-dist = [ [[package]] name = "microsoft-teams-graph" -version = "0.0.1a1" +version = "0.0.1a2" source = { editable = "packages/graph" } dependencies = [ { name = "azure-core" },