fix(agentbay): use Pydantic RootModel[Any] for browser extract schema #35
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Uses GitHub Models API for AI release notes. | |
| # Requires: Repository secret MODELS_TOKEN (PAT with models:read scope) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: Release type to cut | |
| required: true | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - patch | |
| - minor | |
| - major | |
| prerelease: | |
| description: Mark the GitHub Release as a prerelease | |
| required: true | |
| default: false | |
| type: boolean | |
| use_ai_notes: | |
| description: Use GitHub Models to draft release notes when MODELS_TOKEN is configured | |
| required: true | |
| default: true | |
| type: boolean | |
| pull_request: | |
| types: | |
| - closed | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: release-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| propose_release: | |
| name: Propose release | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Resolve base tag and target version | |
| id: version | |
| shell: bash | |
| env: | |
| REQUESTED_RELEASE_TYPE: ${{ inputs.release_type }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --force --tags | |
| stable_tag="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" | |
| base_tag="$stable_tag" | |
| if [ -z "$base_tag" ]; then | |
| base_tag="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$' | head -n 1 || true)" | |
| fi | |
| if [ -z "$base_tag" ]; then | |
| base_tag="v0.0.0" | |
| log_range="" | |
| else | |
| log_range="${base_tag}..HEAD" | |
| fi | |
| if [ -n "$log_range" ] && [ -z "$(git log --oneline "$log_range")" ]; then | |
| echo "No commits found since ${base_tag}; skipping duplicate release." | |
| exit 1 | |
| fi | |
| release_type="$REQUESTED_RELEASE_TYPE" | |
| if [ "$release_type" = "auto" ]; then | |
| if [ -n "$log_range" ]; then | |
| subjects="$(git log --format=%s "$log_range")" | |
| bodies="$(git log --format=%B "$log_range")" | |
| else | |
| subjects="$(git log --format=%s)" | |
| bodies="$(git log --format=%B)" | |
| fi | |
| if printf '%s\n' "$bodies" | grep -Eq 'BREAKING CHANGE|^[^[:space:]]+(\([^)]+\))?!:'; then | |
| release_type="major" | |
| elif printf '%s\n' "$subjects" | grep -Eq '^feat(\([^)]+\))?:'; then | |
| release_type="minor" | |
| else | |
| release_type="patch" | |
| fi | |
| fi | |
| next_version="$(python - "$base_tag" "$release_type" <<'PY' | |
| import re | |
| import sys | |
| base_tag, release_type = sys.argv[1], sys.argv[2] | |
| match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", base_tag) | |
| if not match: | |
| major, minor, patch = 0, 0, 0 | |
| else: | |
| major, minor, patch = map(int, match.groups()) | |
| if release_type == "major": | |
| major += 1 | |
| minor = 0 | |
| patch = 0 | |
| elif release_type == "minor": | |
| minor += 1 | |
| patch = 0 | |
| else: | |
| patch += 1 | |
| print(f"{major}.{minor}.{patch}") | |
| PY | |
| )" | |
| tag_name="v${next_version}" | |
| if git rev-parse "$tag_name" >/dev/null 2>&1; then | |
| echo "Tag ${tag_name} already exists." | |
| exit 1 | |
| fi | |
| { | |
| echo "base_tag=$base_tag" | |
| echo "release_type=$release_type" | |
| echo "version=$next_version" | |
| echo "tag=$tag_name" | |
| echo "log_range=$log_range" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Base tag: $base_tag" | |
| echo "Release type: $release_type" | |
| echo "Next version: $next_version" | |
| - name: Collect release context | |
| shell: bash | |
| env: | |
| BASE_TAG: ${{ steps.version.outputs.base_tag }} | |
| TARGET_VERSION: ${{ steps.version.outputs.version }} | |
| TARGET_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_TYPE: ${{ steps.version.outputs.release_type }} | |
| LOG_RANGE: ${{ steps.version.outputs.log_range }} | |
| SOURCE_REF: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .github/release-artifacts | |
| if [ -n "$LOG_RANGE" ]; then | |
| git log --no-merges --pretty=format:'- %s (%h)' "$LOG_RANGE" > .github/release-artifacts/commit-bullets.txt | |
| git log --no-merges --pretty=format:'%H%x09%s' "$LOG_RANGE" > .github/release-artifacts/commit-table.tsv | |
| else | |
| git log --no-merges --pretty=format:'- %s (%h)' > .github/release-artifacts/commit-bullets.txt | |
| git log --no-merges --pretty=format:'%H%x09%s' > .github/release-artifacts/commit-table.tsv | |
| fi | |
| python <<'PY' | |
| from pathlib import Path | |
| import os | |
| base_tag = os.environ["BASE_TAG"] | |
| target_version = os.environ["TARGET_VERSION"] | |
| target_tag = os.environ["TARGET_TAG"] | |
| release_type = os.environ["RELEASE_TYPE"] | |
| notes_path = Path("RELEASE_NOTES.md") | |
| existing = notes_path.read_text(encoding="utf-8") if notes_path.exists() else "" | |
| style_excerpt = "\n".join(existing.splitlines()[:120]).strip() | |
| commit_bullets = Path(".github/release-artifacts/commit-bullets.txt").read_text(encoding="utf-8").strip() | |
| # Limit commit bullets to fit within API token limits | |
| lines = commit_bullets.splitlines() | |
| if len(lines) > 100: | |
| lines = lines[:100] + [f"- ... and {len(lines) - 100} more commits"] | |
| commit_summary = "\n".join(lines) | |
| prompt = f"""You are writing Clawith release notes in markdown. | |
| Based on the provided commit history, analyze the changes between the previous version ({base_tag}) and the new target version ({target_tag}), and write comprehensive release notes in Markdown. | |
| Structure of the Release Notes: | |
| 1. Title: Start with a top-level heading exactly formatted as: # {target_tag} — <Concise title summarizing the main theme of this release> | |
| 2. ## What's New: | |
| - Group related changes into thematic subheadings (e.g., ### Core Features, ### UI/UX Enhancements, ### Optimizations). | |
| - Sort subheadings and items within each group by importance: major features first, then enhancements, then minor tweaks. | |
| - Specifically list all newly added features and optimization items. Explain what value they add. | |
| 3. ## Bug Fixes: | |
| - List resolved bugs, issues, or stability improvements. | |
| 4. ## Upgrade Guide: | |
| - Provide standard deployment instructions for upgrading to this version (e.g., rebuilding frontend, restart commands for Docker/Source/Kubernetes). | |
| - Do NOT include manual database migration commands (such as `alembic upgrade heads`), as database migrations run automatically on application startup. | |
| - Mimic the exact formatting, sections, and command blocks shown in the Style Reference below (excluding any manual database migration steps). | |
| 5. ## Notes: | |
| - Add any deployment warnings, dependency updates, or configuration warnings. | |
| Writing Style Rules: | |
| - Keep the tone concise, professional, and product-focused. | |
| - Prefer grouping related commits and summarizing the feature/improvement instead of listing every commit verbatim. | |
| - Do NOT invent or hallucinate any features or fixes that are not present or strongly implied in the commit list. | |
| - Keep the language clean and consistent with previous release notes. | |
| - IMPORTANT: Within each section (What's New, Bug Fixes, etc.), sort items by impact and importance in descending order. New core features and major enhancements come first, followed by smaller improvements. Bug fixes that affect stability or data integrity come before minor UI tweaks. | |
| Style Reference (Mimic this structure and formatting): | |
| --- | |
| {style_excerpt} | |
| --- | |
| Context: | |
| - Previous Release Tag: {base_tag} | |
| - Target Release Tag (Target Version): {target_tag} | |
| - Release Type: {release_type} | |
| - Source Branch: {os.environ["SOURCE_REF"]} | |
| Commits included in this release: | |
| {commit_summary or "- No commits collected"} | |
| """ | |
| Path(".github/release-artifacts/release-prompt.txt").write_text(prompt, encoding="utf-8") | |
| PY | |
| - name: Update version files | |
| shell: bash | |
| env: | |
| TARGET_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| printf '%s\n' "$TARGET_VERSION" > backend/VERSION | |
| printf '%s\n' "$TARGET_VERSION" > frontend/VERSION | |
| - name: Draft release notes with GitHub Models | |
| if: ${{ inputs.use_ai_notes }} | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.MODELS_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| python <<'PY' | |
| from pathlib import Path | |
| import json | |
| prompt = Path(".github/release-artifacts/release-prompt.txt").read_text(encoding="utf-8") | |
| payload = { | |
| "model": "openai/gpt-4.1", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "You write concise, accurate release notes in markdown." | |
| }, | |
| { | |
| "role": "user", | |
| "content": prompt | |
| } | |
| ] | |
| } | |
| Path(".github/release-artifacts/openai-payload.json").write_text( | |
| json.dumps(payload, ensure_ascii=False), | |
| encoding="utf-8", | |
| ) | |
| PY | |
| status_code=$( | |
| curl -sS -L \ | |
| -o .github/release-artifacts/openai-response.json \ | |
| -w "%{http_code}" \ | |
| -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "X-GitHub-Api-Version: 2026-03-10" \ | |
| -H "Content-Type: application/json" \ | |
| https://models.github.ai/inference/chat/completions \ | |
| -d @.github/release-artifacts/openai-payload.json | |
| ) | |
| echo "HTTP status: $status_code" | |
| cat .github/release-artifacts/openai-response.json | |
| test "$status_code" -lt 400 | |
| python <<'PY' | |
| from pathlib import Path | |
| import json | |
| response = json.loads(Path(".github/release-artifacts/openai-response.json").read_text(encoding="utf-8")) | |
| text = response.get("choices", [{}])[0].get("message", {}).get("content", "").strip() | |
| if not text: | |
| raise SystemExit("GitHub Models did not return release note text.") | |
| Path(".github/release-artifacts/release-notes.generated.md").write_text( | |
| text.rstrip() + "\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| - name: Build fallback release notes | |
| shell: bash | |
| env: | |
| BASE_TAG: ${{ steps.version.outputs.base_tag }} | |
| TARGET_VERSION: ${{ steps.version.outputs.version }} | |
| TARGET_TAG: ${{ steps.version.outputs.tag }} | |
| SOURCE_REF: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ -s .github/release-artifacts/release-notes.generated.md ]; then | |
| exit 0 | |
| fi | |
| python <<'PY' | |
| from pathlib import Path | |
| import os | |
| base_tag = os.environ["BASE_TAG"] | |
| target_version = os.environ["TARGET_VERSION"] | |
| target_tag = os.environ["TARGET_TAG"] | |
| source_ref = os.environ["SOURCE_REF"] | |
| entries = [] | |
| for line in Path(".github/release-artifacts/commit-table.tsv").read_text(encoding="utf-8").splitlines(): | |
| if not line.strip(): | |
| continue | |
| _, subject = line.split("\t", 1) | |
| entries.append(subject.strip()) | |
| features = [] | |
| fixes = [] | |
| others = [] | |
| for subject in entries: | |
| lowered = subject.lower() | |
| if lowered.startswith("feat"): | |
| features.append(subject) | |
| elif lowered.startswith("fix"): | |
| fixes.append(subject) | |
| else: | |
| others.append(subject) | |
| def bullets(items): | |
| return "\n".join(f"- {item}" for item in items[:8]) or "- No user-facing highlights captured from commit subjects." | |
| sections = [ | |
| f"# {target_tag} — Release Highlights", | |
| "", | |
| "## What's New", | |
| bullets(features or others), | |
| ] | |
| if fixes: | |
| sections.extend([ | |
| "", | |
| "## Bug Fixes", | |
| bullets(fixes), | |
| ]) | |
| sections.extend([ | |
| "", | |
| "## Upgrade Guide", | |
| "", | |
| "### Docker Deployment", | |
| "```bash", | |
| f"git pull origin {source_ref}", | |
| "docker compose down && docker compose up -d --build", | |
| "```", | |
| "", | |
| "### Source Deployment", | |
| "```bash", | |
| f"git pull origin {source_ref}", | |
| "cd frontend && npm install && npm run build", | |
| "cd ..", | |
| "```", | |
| "", | |
| "## Notes", | |
| f"- Release generated from changes since `{base_tag}`.", | |
| f"- Runtime version files were updated to `{target_version}`.", | |
| ]) | |
| Path(".github/release-artifacts/release-notes.generated.md").write_text( | |
| "\n".join(sections).rstrip() + "\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| - name: Refresh RELEASE_NOTES.md | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python <<'PY' | |
| from pathlib import Path | |
| notes_file = Path(".github/release-artifacts/release-notes.generated.md") | |
| release_notes_path = Path("RELEASE_NOTES.md") | |
| new_block = notes_file.read_text(encoding="utf-8").strip() | |
| existing = release_notes_path.read_text(encoding="utf-8").strip() if release_notes_path.exists() else "" | |
| if existing: | |
| combined = f"{new_block}\n\n---\n\n{existing}\n" | |
| else: | |
| combined = f"{new_block}\n" | |
| release_notes_path.write_text(combined, encoding="utf-8") | |
| PY | |
| - name: Commit release metadata | |
| shell: bash | |
| env: | |
| TARGET_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| git add backend/VERSION frontend/VERSION RELEASE_NOTES.md | |
| if git diff --cached --quiet; then | |
| echo "No release metadata changes to commit." | |
| exit 0 | |
| fi | |
| git checkout -b "release/${TARGET_TAG}" | |
| git commit -m "chore(release): cut ${TARGET_TAG}" | |
| git push origin "release/${TARGET_TAG}" | |
| - name: Create Pull Request | |
| shell: bash | |
| env: | |
| TARGET_TAG: ${{ steps.version.outputs.tag }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| release_notes="" | |
| if [ -s .github/release-artifacts/release-notes.generated.md ]; then | |
| release_notes="$(cat .github/release-artifacts/release-notes.generated.md)" | |
| fi | |
| gh pr create \ | |
| --title "chore(release): cut ${TARGET_TAG}" \ | |
| --body "$(cat <<EOF | |
| Automated release PR for ${TARGET_TAG}. Merging this PR will automatically tag the release and publish it. | |
| --- | |
| ${release_notes} | |
| EOF | |
| )" \ | |
| --head "release/${TARGET_TAG}" \ | |
| --base "${{ github.ref_name }}" | |
| publish_release: | |
| name: Publish release | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.base.ref }} | |
| fetch-depth: 0 | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Extract Release Info | |
| id: release_info | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| branch_name="${{ github.event.pull_request.head.ref }}" | |
| tag_name="${branch_name#release/}" | |
| python - "$tag_name" <<'PY' | |
| from pathlib import Path | |
| import sys | |
| tag_name = sys.argv[1] | |
| notes_path = Path("RELEASE_NOTES.md") | |
| if not notes_path.exists(): | |
| Path("release-notes.extracted.md").write_text(f"# {tag_name}\n\nRelease cut via merge.", encoding="utf-8") | |
| sys.exit(0) | |
| content = notes_path.read_text(encoding="utf-8") | |
| parts = content.split("\n\n---\n\n") | |
| latest_notes = parts[0].strip() | |
| Path("release-notes.extracted.md").write_text(latest_notes + "\n", encoding="utf-8") | |
| PY | |
| echo "tag=$tag_name" >> "$GITHUB_OUTPUT" | |
| - name: Create and push tag | |
| shell: bash | |
| env: | |
| TARGET_TAG: ${{ steps.release_info.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| git tag -a "$TARGET_TAG" -m "Release $TARGET_TAG" | |
| git push origin "$TARGET_TAG" | |
| - name: Publish GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.release_info.outputs.tag }} | |
| name: ${{ steps.release_info.outputs.tag }} | |
| body_path: release-notes.extracted.md | |
| prerelease: ${{ github.event.pull_request.draft }} | |
| generate_release_notes: false |