From f8b2357c6c7bc9936ecbccc8ee1cc7436dbfdb5c Mon Sep 17 00:00:00 2001 From: mikeangstadt Date: Thu, 26 Mar 2026 10:33:14 -0500 Subject: [PATCH 1/3] fix-auto-update-bugs: Fix auto-update error visibility and add zip target - Add autoUpdater.on("error") handler with gatewayLog.error logging - Replace empty catch blocks with error logging in checkForUpdates calls - Add zip build target alongside DMG for reliable macOS auto-updates - Update release workflow to upload both DMG and zip artifacts - Bump version to 0.8.9 Testing: Ran typecheck and lint, both pass. Verified error handler uses existing gatewayLog infrastructure for consistency. Risks: The zip target addition changes build output -- CI will now produce an additional artifact. electron-updater prefers zip on macOS so this improves update reliability. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 6 ++++-- apps/desktop/electron-builder.yml | 3 +++ apps/desktop/package.json | 2 +- apps/desktop/src/main/app.ts | 13 +++++++++++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6727f9de..e8f6fa70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,12 +83,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit "v${{ steps.version.outputs.version }}" --draft=false --repo "${{ github.repository }}" - - name: Upload DMG as workflow artifact + - name: Upload build artifacts if: steps.check_release.outputs.skip != 'true' uses: actions/upload-artifact@v4 with: name: ClosedLoop-macOS-${{ steps.version.outputs.version }} - path: apps/desktop/dist-dmg/*.dmg + path: | + apps/desktop/dist-dmg/*.dmg + apps/desktop/dist-dmg/*.zip if-no-files-found: error - name: Build Slack message diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index bd5a8fff..9e9ae099 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -20,6 +20,9 @@ mac: - target: dmg arch: - universal + - target: zip + arch: + - universal hardenedRuntime: true entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 946fcac7..6d7ccb24 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.8.8", + "version": "0.8.9", "description": "ClosedLoop Desktop", "author": "ClosedLoop AI ", "private": true, diff --git a/apps/desktop/src/main/app.ts b/apps/desktop/src/main/app.ts index 9eb21639..f25f136a 100644 --- a/apps/desktop/src/main/app.ts +++ b/apps/desktop/src/main/app.ts @@ -256,16 +256,25 @@ export class DesktopApplication { if (app.isPackaged) { autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.on("error", (err) => { + gatewayLog.error("auto-update", `Auto-update error: ${err.message}`); + }); autoUpdater.on("update-available", (info) => { this.desktopWindow.getWindow()?.webContents.send("desktop:update-available", { updateAvailable: true, version: info.version }); }); - void autoUpdater.checkForUpdates().catch(() => {}); + void autoUpdater.checkForUpdates().catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + gatewayLog.error("auto-update", `Failed to check for updates: ${msg}`); + }); if (this.updateCheckTimer) clearInterval(this.updateCheckTimer); this.updateCheckTimer = setInterval(() => { - void autoUpdater.checkForUpdates().catch(() => {}); + void autoUpdater.checkForUpdates().catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + gatewayLog.error("auto-update", `Failed to check for updates: ${msg}`); + }); }, UPDATE_CHECK_INTERVAL_MS); } else { void this.checkForUpdate().then((result) => { From 6ccfae246b5e81c9ab7e4443d0ad1e6e65496277 Mon Sep 17 00:00:00 2001 From: mikeangstadt Date: Thu, 26 Mar 2026 11:24:52 -0500 Subject: [PATCH 2/3] fix-auto-update-bugs: Use OAuth token for code review workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace org-level GitHub App secrets with repo-level CLAUDE_CODE_OAUTH_TOKEN - Use GITHUB_TOKEN (always available) instead of generated app token - Drop private plugin marketplace dependency (requires cross-repo app token) - Simplify workflow to essentials — no org secrets needed for public repo Testing: Verified CLAUDE_CODE_OAUTH_TOKEN exists as repo secret. Risks: Reviews will use default claude-code-action behavior instead of custom plugins. Review quality should be equivalent for this repo's needs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/claude-code-review.yml | 166 +---------------------- 1 file changed, 7 insertions(+), 159 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 51e0d9de..000d7d5e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -31,39 +31,14 @@ jobs: pull-requests: write issues: write actions: read - id-token: write steps: - - name: Generate GitHub App Token - id: generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.CLOSEDLOOP_APP_ID_STAGE }} - private-key: ${{ secrets.CLOSEDLOOP_APP_SECRET_STAGE }} - owner: ${{ github.repository_owner }} - # Access to claude-plugins repo required for private plugin marketplace (code, code-review plugins) - repositories: ${{ github.event.repository.name }},claude-plugins - permission-contents: read - permission-issues: write - permission-pull-requests: write - permission-actions: read - - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - # Full history required so agents can run `git diff` against any base branch fetch-depth: 0 - - name: Restore code review cache - if: github.event.pull_request.number != 0 - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/cr-cache - key: cr-${{ github.event.pull_request.number }}-${{ github.sha }} - restore-keys: | - cr-${{ github.event.pull_request.number }}- - - name: Verify checkout matches PR head if: github.event.pull_request.head.sha != '' shell: bash @@ -79,152 +54,25 @@ jobs: echo "Checkout verified: HEAD is $ACTUAL_SHA" fi - - name: Configure Git for Private Marketplace - shell: bash - run: | - git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/" - - name: Run Claude Code Review id: claude-review continue-on-error: true uses: anthropics/claude-code-action@v1 with: - github_token: ${{ steps.generate_token.outputs.token }} - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_NEW }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} show_full_output: true - plugin_marketplaces: | - https://github.com/closedloop-ai/claude-plugins.git - plugins: | - judges@closedloop-ai - code@closedloop-ai - code-review@closedloop-ai # Allow bot actors like Dependabot to trigger Claude code review allowed_bots: "dependabot,closedloop-ai-stage,closedloop-ai" - prompt: "/code-review:start --github${{ github.event.pull_request.number && format(' {0}', github.event.pull_request.number) || '' }}" - - claude_args: | - --mcp-config '{"mcpServers": {"context7": {"type": "http", "url": "https://mcp.context7.com/mcp"}}}' - --allowedTools "Read,Write,Task,TodoWrite,Skill,Grep,Glob,mcp__context7__resolve-library-id,mcp__context7__query-docs,Bash(gh *),Bash(gh api *),Bash(gh api -X *),Bash(gh api -F *),Bash(gh api -f *),Bash(gh pr *),Bash(git *),Bash(git show *),Bash(git diff *),Bash(git log *),Bash(env *),Bash(grep *),Bash(ls *),Bash(jq *),Bash(cat *),Bash(mkdir *),Bash(echo *),Bash(pnpm *),Bash(npx *),Bash(node *),Bash(tsc *),Bash(python *),Bash(python3 *),Bash(FIND_SCRIPT *),Bash(RESULT *)" + prompt: | + Review this PR for bugs, security issues, and code quality problems. + Focus on correctness and potential regressions. + Post your findings as a PR review comment. - # Sticky comments disabled - was causing streaming mode errors use_sticky_comment: false - - name: Resolve outdated review threads - if: always() - env: - GH_TOKEN: ${{ steps.generate_token.outputs.token }} - shell: bash - run: | - THREADS_FILE=".claude/code-review-threads.json" - if [ ! -f "$THREADS_FILE" ]; then - echo "No threads file — skipping." - exit 0 - fi - HELPERS=$(find "$HOME/.claude" -name "code_review_helpers.py" -path "*/code-review/*" 2>/dev/null | head -1) - if [ -z "$HELPERS" ]; then - echo "Warning: helpers not found — skipping." - exit 0 - fi - python3 "$HELPERS" resolve-threads --threads "$THREADS_FILE" - - - name: Post inline review comments - if: always() - env: - GH_TOKEN: ${{ steps.generate_token.outputs.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - shell: bash - run: | - FINDINGS_FILE=".claude/code-review-findings.json" - if [ ! -f "$FINDINGS_FILE" ]; then - echo "No findings file — skipping." - exit 0 - fi - HELPERS=$(find "$HOME/.claude" -name "code_review_helpers.py" -path "*/code-review/*" 2>/dev/null | head -1) - if [ -z "$HELPERS" ]; then - echo "Warning: helpers not found — skipping." - exit 0 - fi - python3 "$HELPERS" post-comments --findings "$FINDINGS_FILE" - - - name: Post code review summary - id: post-summary - if: always() - env: - GH_TOKEN: ${{ steps.generate_token.outputs.token }} - shell: bash - run: | - PR_NUMBER="${{ github.event.pull_request.number }}" - REPO="${{ github.repository }}" - - if [ -z "$PR_NUMBER" ]; then - echo "No PR number available (workflow_dispatch without PR context). Skipping." - echo "posted=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - SUMMARY_FILE=".claude/code-review-summary.md" - - if [ -f "$SUMMARY_FILE" ]; then - SUMMARY=$(cat "$SUMMARY_FILE") - echo "Found summary file, posting to PR..." - else - # Check if Claude already posted a summary directly (old slash command behavior) - EXISTING=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate --jq ' - [.[] | select( - (.user.login == "closedloop-ai-stage[bot]" or .user.login == "closedloop-ai[bot]" or .user.login == "symphony-cl") and - (.body | startswith("## Code Review Summary")) - )] | length - ' 2>/dev/null || echo "0") - - if [ "$EXISTING" -gt 0 ] 2>/dev/null; then - echo "No summary file found, but Claude already posted $EXISTING summary comment(s) directly. Skipping." - echo "posted=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "No summary file found and no existing summary comment. Posting fallback." - SUMMARY="## Code Review Summary"$'\n\n'"**Status:** Completed"$'\n\n'"Review completed but no summary was generated. Check the workflow logs for details." - fi - - # Post the summary as a PR comment - RESPONSE=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ - -f body="$SUMMARY" 2>&1) - - NEW_COMMENT_ID=$(echo "$RESPONSE" | jq -r '.id // empty') - COMMENT_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty') - if [ -n "$COMMENT_URL" ]; then - echo "Summary posted: $COMMENT_URL" - echo "posted=true" >> "$GITHUB_OUTPUT" - echo "comment_url=$COMMENT_URL" >> "$GITHUB_OUTPUT" - else - echo "Warning: Failed to post summary comment" - echo "$RESPONSE" - echo "posted=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Delete old summary comments (posted by previous runs) to prevent duplicates. - # This runs AFTER posting the new comment so we never lose the summary entirely. - OLD_IDS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate --jq ' - [.[] | select( - (.user.login == "closedloop-ai-stage[bot]" or .user.login == "closedloop-ai[bot]" or .user.login == "symphony-cl") and - (.body | startswith("## Code Review Summary")) - ) | .id][] - ' 2>/dev/null || true) - - for OLD_ID in $OLD_IDS; do - # Skip the comment we just posted - if [ "$OLD_ID" = "$NEW_COMMENT_ID" ]; then - continue - fi - echo "Deleting old summary comment: $OLD_ID" - gh api "repos/${REPO}/issues/comments/$OLD_ID" -X DELETE 2>/dev/null || echo "Warning: Failed to delete comment $OLD_ID" - done - - echo "Summary cleanup complete." - - name: Review completed if: always() run: | From 80ab127414989dd27a3c2324ce5b5642046f2480 Mon Sep 17 00:00:00 2001 From: mikeangstadt Date: Thu, 26 Mar 2026 11:46:58 -0500 Subject: [PATCH 3/3] fix-auto-update-bugs: Revert all workflow changes - Restore claude-code-review.yml to original state - Restore release.yml to original state (revert zip artifact change) - Workflow fixes are out of scope for this PR Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/claude-code-review.yml | 166 ++++++++++++++++++++++- .github/workflows/release.yml | 6 +- 2 files changed, 161 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 000d7d5e..51e0d9de 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -31,14 +31,39 @@ jobs: pull-requests: write issues: write actions: read + id-token: write steps: + - name: Generate GitHub App Token + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.CLOSEDLOOP_APP_ID_STAGE }} + private-key: ${{ secrets.CLOSEDLOOP_APP_SECRET_STAGE }} + owner: ${{ github.repository_owner }} + # Access to claude-plugins repo required for private plugin marketplace (code, code-review plugins) + repositories: ${{ github.event.repository.name }},claude-plugins + permission-contents: read + permission-issues: write + permission-pull-requests: write + permission-actions: read + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} + # Full history required so agents can run `git diff` against any base branch fetch-depth: 0 + - name: Restore code review cache + if: github.event.pull_request.number != 0 + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/cr-cache + key: cr-${{ github.event.pull_request.number }}-${{ github.sha }} + restore-keys: | + cr-${{ github.event.pull_request.number }}- + - name: Verify checkout matches PR head if: github.event.pull_request.head.sha != '' shell: bash @@ -54,25 +79,152 @@ jobs: echo "Checkout verified: HEAD is $ACTUAL_SHA" fi + - name: Configure Git for Private Marketplace + shell: bash + run: | + git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/" + - name: Run Claude Code Review id: claude-review continue-on-error: true uses: anthropics/claude-code-action@v1 with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ steps.generate_token.outputs.token }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_NEW }} show_full_output: true + plugin_marketplaces: | + https://github.com/closedloop-ai/claude-plugins.git + plugins: | + judges@closedloop-ai + code@closedloop-ai + code-review@closedloop-ai # Allow bot actors like Dependabot to trigger Claude code review allowed_bots: "dependabot,closedloop-ai-stage,closedloop-ai" - prompt: | - Review this PR for bugs, security issues, and code quality problems. - Focus on correctness and potential regressions. - Post your findings as a PR review comment. + prompt: "/code-review:start --github${{ github.event.pull_request.number && format(' {0}', github.event.pull_request.number) || '' }}" + + claude_args: | + --mcp-config '{"mcpServers": {"context7": {"type": "http", "url": "https://mcp.context7.com/mcp"}}}' + --allowedTools "Read,Write,Task,TodoWrite,Skill,Grep,Glob,mcp__context7__resolve-library-id,mcp__context7__query-docs,Bash(gh *),Bash(gh api *),Bash(gh api -X *),Bash(gh api -F *),Bash(gh api -f *),Bash(gh pr *),Bash(git *),Bash(git show *),Bash(git diff *),Bash(git log *),Bash(env *),Bash(grep *),Bash(ls *),Bash(jq *),Bash(cat *),Bash(mkdir *),Bash(echo *),Bash(pnpm *),Bash(npx *),Bash(node *),Bash(tsc *),Bash(python *),Bash(python3 *),Bash(FIND_SCRIPT *),Bash(RESULT *)" + # Sticky comments disabled - was causing streaming mode errors use_sticky_comment: false + - name: Resolve outdated review threads + if: always() + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + shell: bash + run: | + THREADS_FILE=".claude/code-review-threads.json" + if [ ! -f "$THREADS_FILE" ]; then + echo "No threads file — skipping." + exit 0 + fi + HELPERS=$(find "$HOME/.claude" -name "code_review_helpers.py" -path "*/code-review/*" 2>/dev/null | head -1) + if [ -z "$HELPERS" ]; then + echo "Warning: helpers not found — skipping." + exit 0 + fi + python3 "$HELPERS" resolve-threads --threads "$THREADS_FILE" + + - name: Post inline review comments + if: always() + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + shell: bash + run: | + FINDINGS_FILE=".claude/code-review-findings.json" + if [ ! -f "$FINDINGS_FILE" ]; then + echo "No findings file — skipping." + exit 0 + fi + HELPERS=$(find "$HOME/.claude" -name "code_review_helpers.py" -path "*/code-review/*" 2>/dev/null | head -1) + if [ -z "$HELPERS" ]; then + echo "Warning: helpers not found — skipping." + exit 0 + fi + python3 "$HELPERS" post-comments --findings "$FINDINGS_FILE" + + - name: Post code review summary + id: post-summary + if: always() + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + shell: bash + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + REPO="${{ github.repository }}" + + if [ -z "$PR_NUMBER" ]; then + echo "No PR number available (workflow_dispatch without PR context). Skipping." + echo "posted=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + SUMMARY_FILE=".claude/code-review-summary.md" + + if [ -f "$SUMMARY_FILE" ]; then + SUMMARY=$(cat "$SUMMARY_FILE") + echo "Found summary file, posting to PR..." + else + # Check if Claude already posted a summary directly (old slash command behavior) + EXISTING=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate --jq ' + [.[] | select( + (.user.login == "closedloop-ai-stage[bot]" or .user.login == "closedloop-ai[bot]" or .user.login == "symphony-cl") and + (.body | startswith("## Code Review Summary")) + )] | length + ' 2>/dev/null || echo "0") + + if [ "$EXISTING" -gt 0 ] 2>/dev/null; then + echo "No summary file found, but Claude already posted $EXISTING summary comment(s) directly. Skipping." + echo "posted=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "No summary file found and no existing summary comment. Posting fallback." + SUMMARY="## Code Review Summary"$'\n\n'"**Status:** Completed"$'\n\n'"Review completed but no summary was generated. Check the workflow logs for details." + fi + + # Post the summary as a PR comment + RESPONSE=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + -f body="$SUMMARY" 2>&1) + + NEW_COMMENT_ID=$(echo "$RESPONSE" | jq -r '.id // empty') + COMMENT_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty') + if [ -n "$COMMENT_URL" ]; then + echo "Summary posted: $COMMENT_URL" + echo "posted=true" >> "$GITHUB_OUTPUT" + echo "comment_url=$COMMENT_URL" >> "$GITHUB_OUTPUT" + else + echo "Warning: Failed to post summary comment" + echo "$RESPONSE" + echo "posted=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Delete old summary comments (posted by previous runs) to prevent duplicates. + # This runs AFTER posting the new comment so we never lose the summary entirely. + OLD_IDS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate --jq ' + [.[] | select( + (.user.login == "closedloop-ai-stage[bot]" or .user.login == "closedloop-ai[bot]" or .user.login == "symphony-cl") and + (.body | startswith("## Code Review Summary")) + ) | .id][] + ' 2>/dev/null || true) + + for OLD_ID in $OLD_IDS; do + # Skip the comment we just posted + if [ "$OLD_ID" = "$NEW_COMMENT_ID" ]; then + continue + fi + echo "Deleting old summary comment: $OLD_ID" + gh api "repos/${REPO}/issues/comments/$OLD_ID" -X DELETE 2>/dev/null || echo "Warning: Failed to delete comment $OLD_ID" + done + + echo "Summary cleanup complete." + - name: Review completed if: always() run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8f6fa70..6727f9de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,14 +83,12 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit "v${{ steps.version.outputs.version }}" --draft=false --repo "${{ github.repository }}" - - name: Upload build artifacts + - name: Upload DMG as workflow artifact if: steps.check_release.outputs.skip != 'true' uses: actions/upload-artifact@v4 with: name: ClosedLoop-macOS-${{ steps.version.outputs.version }} - path: | - apps/desktop/dist-dmg/*.dmg - apps/desktop/dist-dmg/*.zip + path: apps/desktop/dist-dmg/*.dmg if-no-files-found: error - name: Build Slack message