Skip to content

fix(agentbay): use Pydantic RootModel[Any] for browser extract schema #35

fix(agentbay): use Pydantic RootModel[Any] for browser extract schema

fix(agentbay): use Pydantic RootModel[Any] for browser extract schema #35

Workflow file for this run

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