Skip to content
Merged
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
263 changes: 263 additions & 0 deletions .github/workflows/patch-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
name: Auto-Fix Patches

on:
# Triggered by upstream-watch when a new release is detected
workflow_dispatch:
inputs:
new_tag:
description: 'New upstream tag to upgrade to'
required: true
# Can also be called by other workflows
workflow_call:
inputs:
new_tag:
description: 'New upstream tag to upgrade to'
required: true
type: string

jobs:
autofix:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write

steps:
- uses: actions/checkout@v4

- name: Read current config
run: |
source UPSTREAM.conf
echo "OLD_TAG=$UPSTREAM_TAG" >> $GITHUB_ENV
echo "UPSTREAM_REPO=$UPSTREAM_REPO" >> $GITHUB_ENV
echo "NEW_TAG=${{ inputs.new_tag }}" >> $GITHUB_ENV

- name: Check for patches
id: check
run: |
COUNT=$(find patches -name '*.patch' 2>/dev/null | wc -l | tr -d ' ')
echo "patch_count=$COUNT" >> $GITHUB_OUTPUT
if [[ "$COUNT" -eq 0 ]]; then
echo "No patches found — nothing to auto-fix."
fi

- name: Clone upstream at new tag
if: steps.check.outputs.patch_count != '0'
run: |
git clone --depth 50 --branch ${{ env.NEW_TAG }} \
${{ env.UPSTREAM_REPO }} vendor/picoclaw

- name: Try applying all patches
if: steps.check.outputs.patch_count != '0'
id: apply
run: |
cd vendor/picoclaw
FAILED=""
PASSED=""
for p in ../../patches/*.patch; do
NAME="$(basename "$p")"
if git am --3way "$p" 2>/dev/null; then
echo "OK: $NAME"
PASSED="$PASSED $NAME"
else
echo "FAIL: $NAME"
FAILED="$FAILED $NAME"
git am --abort 2>/dev/null || true
fi
# Reset for next independent test
git reset --hard ${{ env.NEW_TAG }} 2>/dev/null
done

echo "failed_patches=$FAILED" >> $GITHUB_OUTPUT
echo "passed_patches=$PASSED" >> $GITHUB_OUTPUT

if [[ -z "$FAILED" ]]; then
echo "all_passed=true" >> $GITHUB_OUTPUT
echo ""
echo "All patches apply cleanly to ${{ env.NEW_TAG }}!"
else
echo "all_passed=false" >> $GITHUB_OUTPUT
echo ""
echo "Failed patches:$FAILED"
fi

# --- Fast path: all patches apply, just update UPSTREAM.conf ---

- name: Update UPSTREAM.conf (all passed)
if: steps.apply.outputs.all_passed == 'true'
run: |
NEW_SHA=$(git ls-remote ${{ env.UPSTREAM_REPO }} \
"refs/tags/${{ env.NEW_TAG }}" | head -1 | awk '{print $1}')
cat > UPSTREAM.conf <<EOF
# Upstream configuration — source of truth for the base version
UPSTREAM_REPO="${{ env.UPSTREAM_REPO }}"
UPSTREAM_TAG="${{ env.NEW_TAG }}"
UPSTREAM_SHA="$NEW_SHA"
EOF
# Remove leading whitespace from heredoc
sed -i 's/^ //' UPSTREAM.conf

- name: Create PR (clean upgrade)
if: steps.apply.outputs.all_passed == 'true'
run: |
BRANCH="auto/upgrade-${{ env.NEW_TAG }}"
git checkout -b "$BRANCH"
git add UPSTREAM.conf
git commit -m "chore: upgrade upstream to ${{ env.NEW_TAG }}

All patches apply cleanly — no regeneration needed.

Patches validated:${{ steps.apply.outputs.passed_patches }}"
git push origin "$BRANCH"

gh pr create \
--title "Upgrade to upstream ${{ env.NEW_TAG }}" \
--label "upstream-upgrade" \
--body "$(cat <<'BODY'
## Summary
- Upstream upgraded from `${{ env.OLD_TAG }}` to `${{ env.NEW_TAG }}`
- All patches apply cleanly — no AI regeneration was needed
- `UPSTREAM.conf` updated to new tag

## Patches validated
${{ steps.apply.outputs.passed_patches }}

---
_Auto-generated by patch-autofix.yml_
BODY
)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# --- Slow path: some patches failed, invoke AI regeneration ---

- name: Setup Node.js
if: steps.apply.outputs.all_passed == 'false'
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install Agent SDK
if: steps.apply.outputs.all_passed == 'false'
run: cd scripts && npm ci

- name: AI-regenerate failed patches
if: steps.apply.outputs.all_passed == 'false'
run: |
REGEN_OK=""
REGEN_FAIL=""

for PATCH_NAME in ${{ steps.apply.outputs.failed_patches }}; do
echo ""
echo "========================================="
echo "Regenerating: $PATCH_NAME"
echo "========================================="

# Reset vendor to clean state
cd vendor/picoclaw
git reset --hard ${{ env.NEW_TAG }}
cd ../..

if node scripts/ai-regenerate-patch-ci.mjs \
"patches/$PATCH_NAME" "${{ env.OLD_TAG }}" "${{ env.NEW_TAG }}"; then
echo "OK: $PATCH_NAME regenerated successfully"
REGEN_OK="$REGEN_OK $PATCH_NAME"
else
echo "FAIL: $PATCH_NAME could not be regenerated"
REGEN_FAIL="$REGEN_FAIL $PATCH_NAME"
fi
done

echo "regen_ok=$REGEN_OK" >> $GITHUB_ENV
echo "regen_fail=$REGEN_FAIL" >> $GITHUB_ENV
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

- name: Validate all patches apply after regeneration
if: steps.apply.outputs.all_passed == 'false'
id: validate
run: |
cd vendor/picoclaw
git reset --hard ${{ env.NEW_TAG }}

ALL_OK=true
for p in ../../patches/*.patch; do
NAME="$(basename "$p")"
echo "Applying: $NAME"
if git am --3way "$p"; then
echo " OK"
else
echo " FAIL (even after regeneration)"
git am --abort 2>/dev/null || true
ALL_OK=false
break
fi
done

echo "validation_passed=$ALL_OK" >> $GITHUB_OUTPUT

- name: Update UPSTREAM.conf
if: steps.apply.outputs.all_passed == 'false'
run: |
NEW_SHA=$(git ls-remote ${{ env.UPSTREAM_REPO }} \
"refs/tags/${{ env.NEW_TAG }}" | head -1 | awk '{print $1}')
cat > UPSTREAM.conf <<EOF
# Upstream configuration — source of truth for the base version
UPSTREAM_REPO="${{ env.UPSTREAM_REPO }}"
UPSTREAM_TAG="${{ env.NEW_TAG }}"
UPSTREAM_SHA="$NEW_SHA"
EOF
sed -i 's/^ //' UPSTREAM.conf

- name: Create PR (with regenerated patches)
if: steps.apply.outputs.all_passed == 'false'
run: |
BRANCH="auto/upgrade-${{ env.NEW_TAG }}"
git checkout -b "$BRANCH"
git add patches/ UPSTREAM.conf
git commit -m "chore: upgrade to upstream ${{ env.NEW_TAG }} with AI-regenerated patches

Patches regenerated by Claude Agent SDK.
Validation: ${{ steps.validate.outputs.validation_passed == 'true' && 'all patches apply' || 'some patches need manual review' }}"
git push origin "$BRANCH"

LABELS="upstream-upgrade,ai-generated"
if [[ "${{ steps.validate.outputs.validation_passed }}" != "true" ]]; then
LABELS="$LABELS,needs-review"
fi
if [[ -n "${{ env.regen_fail }}" ]]; then
LABELS="$LABELS,needs-review"
fi

gh pr create \
--title "Upgrade to upstream ${{ env.NEW_TAG }} (AI-regenerated patches)" \
--label "$LABELS" \
--body "$(cat <<BODY
## Summary
- Upstream upgraded from \`${{ env.OLD_TAG }}\` to \`${{ env.NEW_TAG }}\`
- Some patches required AI regeneration using Claude Agent SDK

## Patch Status

**Already applied cleanly:**
${{ steps.apply.outputs.passed_patches || '(none)' }}

**AI-regenerated successfully:**
${{ env.regen_ok || '(none)' }}

**Failed (needs manual review):**
${{ env.regen_fail || '(none)' }}

## Validation
Full patch sequence applies: **${{ steps.validate.outputs.validation_passed }}**

> Review the regenerated patch diffs carefully. AI regeneration preserves
> intent but may introduce subtle differences.

---
_Auto-generated by patch-autofix.yml using Claude Agent SDK_
BODY
)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88 changes: 88 additions & 0 deletions .github/workflows/patch-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Validate Patches

on:
push:
paths:
- 'patches/**'
- 'UPSTREAM.conf'
pull_request:
paths:
- 'patches/**'
- 'UPSTREAM.conf'
workflow_dispatch:
inputs:
upstream_tag:
description: 'Override upstream tag to test against'
required: false

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Resolve target tag
run: |
source UPSTREAM.conf
TAG="${{ inputs.upstream_tag || '' }}"
if [[ -z "$TAG" ]]; then
TAG="$UPSTREAM_TAG"
fi
echo "TAG=$TAG" >> $GITHUB_ENV
echo "UPSTREAM_REPO=$UPSTREAM_REPO" >> $GITHUB_ENV

- name: Check for patches
id: check
run: |
COUNT=$(find patches -name '*.patch' 2>/dev/null | wc -l | tr -d ' ')
echo "patch_count=$COUNT" >> $GITHUB_OUTPUT
if [[ "$COUNT" -eq 0 ]]; then
echo "No patches found — skipping validation."
fi

- name: Clone upstream at target tag
if: steps.check.outputs.patch_count != '0'
run: git clone --depth 1 --branch ${{ env.TAG }} ${{ env.UPSTREAM_REPO }} vendor/picoclaw

- name: Apply patches
if: steps.check.outputs.patch_count != '0'
run: |
cd vendor/picoclaw
PASS=0
FAIL=0
for p in ../../patches/*.patch; do
NAME="$(basename "$p")"
echo "::group::Applying $NAME"
if git am --3way "$p"; then
echo "OK: $NAME"
PASS=$((PASS + 1))
else
echo "::error::Patch failed: $NAME"
git am --abort || true
FAIL=$((FAIL + 1))
fi
echo "::endgroup::"
done
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi

- name: Build upstream with patches (Go)
if: steps.check.outputs.patch_count != '0'
uses: actions/setup-go@v5
with:
go-version-file: vendor/picoclaw/go.mod

- name: Verify build
if: steps.check.outputs.patch_count != '0'
run: |
cd vendor/picoclaw
go build ./...

- name: Run tests
if: steps.check.outputs.patch_count != '0'
run: |
cd vendor/picoclaw
go test ./... -short -count=1
Loading
Loading