Skip to content
Open
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
40 changes: 27 additions & 13 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Beta version (e.g., 2.8.0-beta.1)'
description: "Beta version (e.g., 2.8.0-beta.1)"
required: true
type: string
dry_run:
description: 'Test build without creating release'
description: "Test build without creating release"
required: false
default: false
type: boolean
Expand Down Expand Up @@ -74,12 +74,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -169,12 +169,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -260,12 +260,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand All @@ -284,7 +284,7 @@ jobs:
- name: Cache pip wheel cache
uses: actions/cache@v4
with:
path: ~\AppData\Local\pip\Cache
path: ~/AppData/Local/pip/Cache
key: pip-wheel-${{ runner.os }}-x64-${{ hashFiles('apps/backend/requirements.txt') }}
restore-keys: |
pip-wheel-${{ runner.os }}-x64-
Expand Down Expand Up @@ -330,12 +330,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -396,7 +396,14 @@ jobs:
apps/frontend/dist/*.yml

create-release:
needs: [create-tag, build-macos-intel, build-macos-arm64, build-windows, build-linux]
needs:
[
create-tag,
build-macos-intel,
build-macos-arm64,
build-windows,
build-linux,
]
runs-on: ubuntu-latest
if: ${{ github.event.inputs.dry_run != 'true' }}
permissions:
Expand Down Expand Up @@ -461,7 +468,14 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

dry-run-summary:
needs: [create-tag, build-macos-intel, build-macos-arm64, build-windows, build-linux]
needs:
[
create-tag,
build-macos-intel,
build-macos-arm64,
build-windows,
build-linux,
]
runs-on: ubuntu-latest
if: ${{ github.event.inputs.dry_run == 'true' }}
steps:
Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ on:
push:
branches: [main]
paths:
- 'apps/frontend/package.json'
- 'package.json'
- "apps/frontend/package.json"
- "package.json"

jobs:
check-and-tag:
Expand Down Expand Up @@ -87,11 +87,17 @@ jobs:
# Extract changelog section for this version
# Looks for "## X.Y.Z" header and captures until next "## " or "---" or end
CHANGELOG_CONTENT=$(awk -v ver="$VERSION" '
BEGIN { found=0; content="" }
BEGIN {
found=0
content=""
# Escape dots in version for regex matching
ver_escaped = ver
gsub(/\./, "\\\\.", ver_escaped)
}
/^## / {
if (found) exit
# Match version at start of header (e.g., "## 2.7.3 -" or "## 2.7.3")
if ($2 == ver || $2 ~ "^"ver"[[:space:]]*-") {
if ($2 == ver || $2 ~ "^"ver_escaped"[[:space:]]*-") {
found=1
# Skip the header line itself, we will add our own
next
Expand Down
58 changes: 14 additions & 44 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ name: Release
on:
push:
tags:
- 'v*'
- "v*"
workflow_dispatch:
inputs:
dry_run:
description: 'Test build without creating release'
description: "Test build without creating release"
required: false
default: true
type: boolean
Expand All @@ -24,12 +24,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -112,12 +112,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -196,12 +196,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -258,12 +258,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"

- name: Get npm cache directory
id: npm-cache
Expand Down Expand Up @@ -516,45 +516,15 @@ jobs:

echo "πŸ“‹ Extracting release notes for version $VERSION from CHANGELOG.md..."

if [ ! -f "$CHANGELOG_FILE" ]; then
echo "::warning::CHANGELOG.md not found, using minimal release notes"
echo "body=Release v$VERSION" >> $GITHUB_OUTPUT
exit 0
fi

# Extract changelog section for this version
# Looks for "## X.Y.Z" header and captures until next "## " or "---"
CHANGELOG_CONTENT=$(awk -v ver="$VERSION" '
BEGIN { found=0; content="" }
/^## / {
if (found) exit
# Match version at start of header (e.g., "## 2.7.3 -" or "## 2.7.3")
if ($2 == ver || $2 ~ "^"ver"[[:space:]]*-") {
found=1
next
}
}
/^---$/ { if (found) exit }
found { content = content $0 "\n" }
END {
if (!found) {
print "NOT_FOUND"
exit 0
}
# Trim leading/trailing whitespace
gsub(/^[[:space:]]+|[[:space:]]+$/, "", content)
print content
}
' "$CHANGELOG_FILE")

if [ "$CHANGELOG_CONTENT" = "NOT_FOUND" ] || [ -z "$CHANGELOG_CONTENT" ]; then
# Use shared extraction script with proper regex escaping
if CHANGELOG_CONTENT=$(bash scripts/extract-changelog.sh "$VERSION" "$CHANGELOG_FILE"); then
echo "βœ… Extracted changelog content"
else
echo "::warning::Version $VERSION not found in CHANGELOG.md, using minimal release notes"
REPO="${{ github.repository }}"
CHANGELOG_CONTENT="Release v$VERSION"$'\n\n'"See [CHANGELOG.md](https://github.com/${REPO}/blob/main/CHANGELOG.md) for details."
fi

echo "βœ… Extracted changelog content"

# Save to file first (more reliable for multiline)
echo "$CHANGELOG_CONTENT" > changelog-body.md

Expand Down
28 changes: 28 additions & 0 deletions apps/backend/core/worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,34 @@ def merge_worktree(
self._run_git(["merge", "--abort"])
return False

# NEW: Verify merge commit was created (for non-staged merges)
if not no_commit:
# Get the latest commit message to verify merge succeeded
verify_result = self._run_git(["log", "-1", "--format=%s"])
if verify_result.returncode == 0:
latest_commit_msg = verify_result.stdout.strip()
expected_msg = f"auto-claude: Merge {info.branch}"

if latest_commit_msg != expected_msg:
print(f"Warning: Expected merge commit message not found.")
print(f"Expected: '{expected_msg}'")
print(f"Got: '{latest_commit_msg}'")

# Check if branch is already merged (acceptable edge case)
check_merged = self._run_git(
["branch", "--merged", self.base_branch, info.branch]
)

if info.branch not in check_merged.stdout:
print(f"Error: Branch {info.branch} is not fully merged into {self.base_branch}")
print("This may indicate a fast-forward merge or other issue.")
print("Please verify with: git log --oneline -n 10")
return False
else:
print(f"Branch {info.branch} appears to be already merged. Continuing...")
else:
print(f"βœ“ Merge commit created successfully: {latest_commit_msg}")

if no_commit:
# Unstage any files that are gitignored in the main branch
# These get staged during merge because they exist in the worktree branch
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/merge/file_merger.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ def apply_single_task_changes(
if change.change_type == ChangeType.ADD_IMPORT:
# Add import at top
# Use splitlines() to handle all line ending styles (LF, CRLF, CR)
# Detect and preserve original line ending style
original_ending = "\r\n" if "\r\n" in content else "\n"
lines = content.splitlines()
import_end = find_import_end(lines, file_path)
lines.insert(import_end, change.content_after)
content = "\n".join(lines)
content = original_ending.join(lines)
elif change.change_type == ChangeType.ADD_FUNCTION:
# Add function at end (before exports)
content += f"\n\n{change.content_after}"
Expand Down
43 changes: 29 additions & 14 deletions apps/backend/merge/semantic_analysis/regex_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@
from ..types import ChangeType, FileAnalysis, SemanticChange


def _extract_func_names_from_matches(
matches: list[str | tuple[str, ...]]
) -> set[str]:
"""
Extract function names from regex findall matches.

For patterns with alternation, findall() returns tuples.
This helper extracts the non-empty match from each tuple.

Args:
matches: List of matches from re.findall(), which can be strings or tuples

Returns:
Set of extracted function names
"""
names = set()
for match in matches:
if isinstance(match, tuple):
# Get the first non-empty group from the tuple
name = next((m for m in match if m), None)
if name:
names.add(name)
elif match:
names.add(match)
return names


def analyze_with_regex(
file_path: str,
before: str,
Expand Down Expand Up @@ -96,20 +123,8 @@ def analyze_with_regex(
if func_pattern:
# For JS/TS patterns with alternation, findall() returns tuples
# Extract the non-empty match from each tuple
def extract_func_names(matches):
names = set()
for match in matches:
if isinstance(match, tuple):
# Get the first non-empty group from the tuple
name = next((m for m in match if m), None)
if name:
names.add(name)
elif match:
names.add(match)
return names

funcs_before = extract_func_names(func_pattern.findall(before_normalized))
funcs_after = extract_func_names(func_pattern.findall(after_normalized))
funcs_before = _extract_func_names_from_matches(func_pattern.findall(before_normalized))
funcs_after = _extract_func_names_from_matches(func_pattern.findall(after_normalized))

for func in funcs_after - funcs_before:
changes.append(
Expand Down
Loading