diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index d1c662bc..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[bumpversion] -current_version = 0.0.1 -commit = True -tag = True - -[bumpversion:file:pyproject.toml] -search = version = "{current_version}" -replace = version = "{new_version}" - -[bumpversion:file:stagehand/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" \ No newline at end of file diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..927e0604 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,11 @@ +# Changesets + +This directory contains changeset files that track changes. + +## Creating a changeset + +Run `changeset` or `changeset add` to create a new changeset. + +## More info + +See https://github.com/browserbase/pychangeset for more information. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..cd536e07 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,17 @@ +{ + "changeTypes": { + "major": { + "description": "Breaking changes", + "emoji": "\ud83d\udca5" + }, + "minor": { + "description": "New features", + "emoji": "\u2728" + }, + "patch": { + "description": "Bug fixes and improvements", + "emoji": "\ud83d\udc1b" + } + }, + "baseBranch": "main" +} \ No newline at end of file diff --git a/.changeset/functional-pink-centipede.md b/.changeset/functional-pink-centipede.md new file mode 100644 index 00000000..8545089e --- /dev/null +++ b/.changeset/functional-pink-centipede.md @@ -0,0 +1,5 @@ +--- +"stagehand": patch +--- + +start using pychangeset to track changes diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml new file mode 100644 index 00000000..342dd467 --- /dev/null +++ b/.github/workflows/changesets.yml @@ -0,0 +1,140 @@ +name: Changesets + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + changesets: + name: Create or Update Release PR + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Check for changesets + id: check_changesets + run: | + if ls .changeset/*.md 2>/dev/null | grep -v README.md > /dev/null; then + echo "has_changesets=true" >> $GITHUB_OUTPUT + else + echo "has_changesets=false" >> $GITHUB_OUTPUT + fi + + - name: Get PR metadata + if: steps.check_changesets.outputs.has_changesets == 'true' + id: pr_metadata + run: | + # Get the merge commit info + COMMIT_SHA="${{ github.sha }}" + echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV + + # Try to extract PR info from commit message + PR_NUMBER=$(git log -1 --pretty=%B | grep -oP '(?<=#)\d+' | head -1 || echo "") + if [ -n "$PR_NUMBER" ]; then + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + + # Get PR author using GitHub API + PR_AUTHOR=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER --jq '.user.login' || echo "") + echo "PR_AUTHOR=$PR_AUTHOR" >> $GITHUB_ENV + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate changelogs and PR description + if: steps.check_changesets.outputs.has_changesets == 'true' + run: | + # Generate changelogs and PR description + uvx changeset changelog --output-pr-description pr-description.md + + # Save PR description for later use + echo "PR_DESCRIPTION<> $GITHUB_ENV + cat pr-description.md >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + rm pr-description.md + + - name: Bump versions + if: steps.check_changesets.outputs.has_changesets == 'true' + run: | + uvx changeset version --skip-changelog + + - name: Commit changes + if: steps.check_changesets.outputs.has_changesets == 'true' + id: commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Add all changes + git add . + + # Commit if there are changes + if ! git diff --cached --quiet; then + git commit -m "Version packages and update changelogs" + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Force push to changeset branch + if: steps.check_changesets.outputs.has_changesets == 'true' && steps.commit.outputs.has_changes == 'true' + run: | + # Force push to the changeset-release branch + git push origin HEAD:changeset-release --force + + - name: Create or update PR + if: steps.check_changesets.outputs.has_changesets == 'true' && steps.commit.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:changeset-release`, + base: 'main', + state: 'open' + }); + + const prBody = process.env.PR_DESCRIPTION; + const prTitle = '🚀 Release packages'; + + if (prs.length > 0) { + // Update existing PR + const pr = prs[0]; + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + title: prTitle, + body: prBody + }); + console.log(`Updated PR #${pr.number}`); + } else { + // Create new PR + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: prTitle, + body: prBody, + head: 'changeset-release', + base: 'main' + }); + console.log(`Created PR #${pr.number}`); + } diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml new file mode 100644 index 00000000..e328f661 --- /dev/null +++ b/.github/workflows/check-changeset.yml @@ -0,0 +1,26 @@ +name: Check Changeset + +on: + pull_request: + types: [opened, synchronize] + +jobs: + check-changeset: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Check for changeset + run: | + uvx changeset check-changeset \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index fd23b6ad..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Publish to PyPI - -on: - workflow_dispatch: - inputs: - release_type: - description: 'Release type (patch, minor, major)' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - create_release: - description: 'Create GitHub Release' - required: true - default: true - type: boolean - -permissions: - contents: write - -jobs: - build-and-publish: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine wheel setuptools ruff black - pip install -r requirements.txt - if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - # TODO add playwright install for CI pytest - - - name: Run linting and formatting - run: | - # Run linter - black --check --diff stagehand - - # Run Ruff formatter check (without modifying files) - ruff check stagehand - - # TODO: add back as soon as CI is passing - # - name: Run tests - # run: | - # pytest - - - name: Calculate new version - id: version - run: | - # Get current version from pyproject.toml - CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") - echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - # Parse version components - IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" - - # Calculate new version based on release type - case "${{ github.event.inputs.release_type }}" in - "major") - NEW_MAJOR=$((MAJOR + 1)) - NEW_MINOR=0 - NEW_PATCH=0 - ;; - "minor") - NEW_MAJOR=$MAJOR - NEW_MINOR=$((MINOR + 1)) - NEW_PATCH=0 - ;; - "patch") - NEW_MAJOR=$MAJOR - NEW_MINOR=$MINOR - NEW_PATCH=$((PATCH + 1)) - ;; - esac - - NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "Bumping version from $CURRENT_VERSION to $NEW_VERSION" - - - name: Update version files - run: | - CURRENT_VERSION="${{ steps.version.outputs.current_version }}" - NEW_VERSION="${{ steps.version.outputs.new_version }}" - - # Update pyproject.toml - sed -i "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml - - # Update __init__.py - sed -i "s/__version__ = \"$CURRENT_VERSION\"/__version__ = \"$NEW_VERSION\"/" stagehand/__init__.py - - echo "Updated version to $NEW_VERSION in pyproject.toml and __init__.py" - - - name: Configure Git - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - - name: Commit version bump - run: | - git add pyproject.toml stagehand/__init__.py - git commit -m "Bump version to ${{ steps.version.outputs.new_version }}" - git tag "v${{ steps.version.outputs.new_version }}" - - - name: Build package - run: | - python -m build - - - name: Upload to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload dist/* - - - name: Push version bump - run: | - git push - git push --tags - - - name: Create GitHub Release - if: ${{ github.event.inputs.create_release == 'true' }} - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ steps.version.outputs.new_version }} - name: Release v${{ steps.version.outputs.new_version }} - generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..adea3e6d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,123 @@ +name: Release + +on: + pull_request: + types: [closed] + branches: + - main + +jobs: + release: + name: Release packages + # Only run when changeset PR is merged + if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'changeset-release' + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: write + id-token: write # For PyPI trusted publishing + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Build packages + run: | + # Find all packages with pyproject.toml + for pyproject in $(find . -name "pyproject.toml" -not -path "./.venv/*" -not -path "./node_modules/*"); do + dir=$(dirname "$pyproject") + echo "Building package in $dir" + (cd "$dir" && uv build) + done + + - name: Get version info + id: versions + run: | + # Extract version info from PR body + # This is a simplified version - you might want to make it more robust + echo "Extracting version information..." + + # For each package, get its version + RELEASE_TAGS="" + for pyproject in $(find . -name "pyproject.toml" -not -path "./.venv/*" -not -path "./node_modules/*"); do + dir=$(dirname "$pyproject") + # Extract package name and version + PACKAGE_NAME=$(python -c "import tomllib; print(tomllib.load(open('$pyproject', 'rb'))['project']['name'])") + PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('$pyproject', 'rb'))['project']['version'])") + + # Add to release tags + TAG="${PACKAGE_NAME}-v${PACKAGE_VERSION}" + RELEASE_TAGS="${RELEASE_TAGS}${TAG} " + + echo "Package: $PACKAGE_NAME @ $PACKAGE_VERSION" + done + + echo "release_tags=$RELEASE_TAGS" >> $GITHUB_OUTPUT + + - name: Publish to PyPI + run: | + # Publish each package + for pyproject in $(find . -name "pyproject.toml" -not -path "./.venv/*" -not -path "./node_modules/*"); do + dir=$(dirname "$pyproject") + echo "Publishing package in $dir" + (cd "$dir" && uv publish) + done + + - name: Create git tags + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create tags for each package + for pyproject in $(find . -name "pyproject.toml" -not -path "./.venv/*" -not -path "./node_modules/*"); do + PACKAGE_NAME=$(python -c "import tomllib; print(tomllib.load(open('$pyproject', 'rb'))['project']['name'])") + PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('$pyproject', 'rb'))['project']['version'])") + TAG="${PACKAGE_NAME}-v${PACKAGE_VERSION}" + + # Create and push tag + git tag -a "$TAG" -m "Release $PACKAGE_NAME v$PACKAGE_VERSION" + git push origin "$TAG" + done + + - name: Create GitHub releases + uses: actions/github-script@v7 + with: + script: | + // Get the PR body which contains our changelog + const prBody = context.payload.pull_request.body; + + // Parse the PR body to extract package releases + const releaseRegex = /## (.+)@(.+)\n([\s\S]*?)(?=\n## |$)/g; + let match; + + while ((match = releaseRegex.exec(prBody)) !== null) { + const packageName = match[1]; + const version = match[2]; + const changelog = match[3].trim(); + const tag = `${packageName}-v${version}`; + + try { + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + name: `${packageName} v${version}`, + body: changelog, + draft: false, + prerelease: false + }); + console.log(`Created release for ${tag}`); + } catch (error) { + console.error(`Failed to create release for ${tag}:`, error); + } + } diff --git a/examples/agent_example.py b/examples/agent_example.py index 8b214496..c9f0a246 100644 --- a/examples/agent_example.py +++ b/examples/agent_example.py @@ -7,8 +7,7 @@ from rich.panel import Panel from rich.theme import Theme -from stagehand import Stagehand, StagehandConfig, AgentConfig, configure_logging -from stagehand.schemas import AgentExecuteOptions, AgentProvider +from stagehand import Stagehand, StagehandConfig, configure_logging # Create a custom theme for consistent styling custom_theme = Theme( diff --git a/examples/example.py b/examples/example.py index 78219966..c0067929 100644 --- a/examples/example.py +++ b/examples/example.py @@ -1,11 +1,11 @@ import asyncio import logging import os + +from dotenv import load_dotenv from rich.console import Console from rich.panel import Panel from rich.theme import Theme -import json -from dotenv import load_dotenv from stagehand import Stagehand, StagehandConfig from stagehand.utils import configure_logging diff --git a/examples/quickstart.py b/examples/quickstart.py index a441cb33..20daf858 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -1,9 +1,10 @@ import asyncio import os + from dotenv import load_dotenv from pydantic import BaseModel, Field -from stagehand import StagehandConfig, Stagehand +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() diff --git a/stagehand/__init__.py b/stagehand/__init__.py index 8f3a5f09..1134abad 100644 --- a/stagehand/__init__.py +++ b/stagehand/__init__.py @@ -1,5 +1,7 @@ """Stagehand - The AI Browser Automation Framework""" +from importlib.metadata import version as get_version + from .agent import Agent from .config import StagehandConfig, default_config from .handlers.observe_handler import ObserveHandler @@ -21,7 +23,7 @@ ObserveResult, ) -__version__ = "0.0.1" +__version__ = get_version("stagehand") __all__ = [ "Stagehand",