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
9 changes: 9 additions & 0 deletions .github/firebase-mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"_comment": "DRAFT — Firebase MCP server config for the crashlytics-autotriage workflow. See docs/PRD-crashlytics-autotriage.md. Requires Application Default Credentials (provided by google-github-actions/auth) with roles/firebasecrashlytics.viewer.",
"mcpServers": {
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp", "--dir", ".", "--project", "openloop-8c266"]
}
}
}
97 changes: 97 additions & 0 deletions .github/workflows/crashlytics-autotriage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Crashlytics auto-triage

# ============================================================================
# DRAFT — NOT YET ENABLED. Pending owner sign-off of docs/PRD-crashlytics-autotriage.md.
# Do not enable until the secrets / service account in the PRD exist.
#
# Trigger: a Crashlytics-sourced GitHub issue (created by the Cloud Function in functions/)
# carrying the `crashlytics-auto` label is opened. Claude reads the crash via the Firebase
# MCP server, posts a root-cause comment, and — when confident — opens a DRAFT PR with a
# first-pass fix. Nothing is ever auto-merged. See the PRD for the full design.
# ============================================================================

on:
issues:
types: [opened, labeled]
workflow_dispatch:
inputs:
issue_number:
description: "Issue number to triage manually (Phase 0 testing)"
required: true

permissions:
contents: write # create branch + commit a candidate fix
pull-requests: write # open the draft PR
issues: write # comment / label the issue
id-token: write # service-account / WIF auth to Google Cloud

concurrency:
group: crashlytics-triage-${{ github.event.issue.number || github.event.inputs.issue_number }}
cancel-in-progress: false

jobs:
triage:
# Only run for Crashlytics-sourced issues (label gate) or an explicit manual dispatch,
# so human-filed issues never spend tokens.
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'opened' &&
contains(join(github.event.issue.labels.*.name, ','), 'crashlytics-auto')) ||
(github.event.action == 'labeled' && github.event.label.name == 'crashlytics-auto')
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }} # for the `gh` CLI used by Claude
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

# Lets the Firebase MCP server read Crashlytics. google-github-actions/auth
# exports GOOGLE_APPLICATION_CREDENTIALS, which firebase-tools picks up as
# Application Default Credentials.
#
# Phase 0 (simple): a service-account key JSON stored as the secret GCP_SA_KEY.
# More secure later: swap credentials_json for keyless Workload Identity Federation:
# with:
# workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
# service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Triage the crash with Claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
You are triaging an automated Crashlytics crash report filed as GitHub issue
#${{ github.event.issue.number || github.event.inputs.issue_number }} in stozo04/OpenLoop.

Follow this repo's CLAUDE.md and docs/DEFINITION_OF_DONE.md.

Steps:
1. Read the issue: `gh issue view ${{ github.event.issue.number || github.event.inputs.issue_number }}`.
Extract the `Crashlytics-Issue-ID:` value from the body.
2. Use the Firebase MCP Crashlytics tools (crashlytics_get_issue, crashlytics_list_events,
crashlytics_batch_get_events) to fetch the stacktrace and a few sample events for that
issue ID. If an MCP call fails (the tools are Experimental), proceed with whatever
metadata is in the issue body and say so.
3. Read the relevant source and docs/lessons_learned/ (several past lessons are
Crashlytics-sourced) to locate the likely cause.
4. ALWAYS post a root-cause comment on the issue with: the failing stack frame, the most
likely cause, and the file(s) involved. Use `gh issue comment`.
5. Decision:
- HIGH confidence (you can point to a specific cause in the code): create a branch
`feature/crashlytics-<shortId>`, make the MINIMAL fix, push, and open a **DRAFT** PR
with `gh pr create --draft`. The PR body must say "Fixes #<issue>", list which
DEFINITION_OF_DONE steps were NOT run on CI (release build, emulator run + screenshot,
instrumented tests), and include the manual QA checklist. NEVER mark it ready; NEVER merge.
- LOW confidence (OEM-specific, OOM, third-party SDK, or unclear): do NOT write code.
Add the `needs-human` label (`gh issue edit ... --add-label needs-human`) and explain why.
6. Be honest about uncertainty. A wrong patch that looks authoritative is worse than none.
claude_args: |
--mcp-config .github/firebase-mcp.json
--allowedTools "mcp__firebase__crashlytics_get_issue,mcp__firebase__crashlytics_list_events,mcp__firebase__crashlytics_batch_get_events,mcp__firebase__crashlytics_get_report,mcp__firebase__crashlytics_list_notes,Bash,Read,Edit,Write,Glob,Grep"
--max-turns 25
--model claude-sonnet-4-6
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ docs/local/

# E2E loop source video — local test asset, never commit (see .claude/commands/e2e-fold-loop.md)
google-pro-fold-video.mp4

# Cloud Functions (Firebase) — dependencies are regenerated by `npm install` and
# excluded from deploy via firebase.json; never commit them.
functions/node_modules/
.firebase/
firebase-debug.log
functions/firebase-debug.log

# Google service-account key (private credential) — NEVER commit
openloop-google-service-account-key-DO-NOT-COMMIT.json
*service-account*.json
*-DO-NOT-COMMIT.*
83 changes: 83 additions & 0 deletions docs/FIREBASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Firebase — Crashlytics auto-triage runbook

Operational notes for the Crashlytics → GitHub → Claude auto-triage system. Design and rationale live in [`PRD-crashlytics-autotriage.md`](PRD-crashlytics-autotriage.md); this file is the day-to-day "what is it and how do I fix it" reference.

**Firebase project:** `openloop-8c266` · **Repo:** `stozo04/OpenLoop` · **Plan:** Blaze (required for the alert function)
**Project Console:** <https://console.firebase.google.com/project/openloop-8c266/overview>

---

## ⚠️ FIRST THING TO CHECK IF IT STOPS WORKING: the GitHub token expires

The `GITHUB_TOKEN` secret the Cloud Function uses to file GitHub issues is a **fine-grained Personal Access Token created with a 1-year expiration** (created June 2026 → **expires around June 2027** — confirm the exact date at <https://github.com/settings/personal-access-tokens>).

**When that token expires, crashes will silently stop becoming GitHub issues.** No crash, no error popup — the function just starts getting `401 Unauthorized` from GitHub and logs it. So if auto-triage goes quiet, **check the token first.**

### How to renew the token (2 minutes)

1. <https://github.com/settings/personal-access-tokens> → create a new fine-grained token.
- Resource owner `stozo04`, repository **OpenLoop** only, permission **Issues: Read and write**.
2. Update the secret (paste the new token at the hidden prompt — the argument is the *name*, not the value):

```
firebase functions:secrets:set GITHUB_TOKEN
```

3. Re-deploy so the function binds the new secret version:

```
firebase deploy --only functions:crashlytics-autotriage
```

> Never paste a token into a terminal command, a file, or a chat. Only into the hidden `Enter a value for GITHUB_TOKEN:` prompt. If a token is ever exposed, revoke it immediately and issue a new one.

---

## What this system is

1. A new **fatal** Crashlytics issue fires a Firebase Alert.
2. The Cloud Function `crashlyticsToGithub` (`functions/index.js`, trigger `onNewFatalIssuePublished`) creates one GitHub issue labeled `crashlytics-auto`.
3. The GitHub Action `.github/workflows/crashlytics-autotriage.yml` wakes Claude, which reads the crash via the Firebase MCP server, comments a root cause, and opens a **draft** PR when confident. Nothing is auto-merged.

## Files

| File | Role |
|------|------|
| `functions/index.js` | The alert listener that files the GitHub issue |
| `functions/package.json` | Node 22 runtime + `firebase-functions` v6 |
| `firebase.json` | Declares the function under codebase `crashlytics-autotriage` |
| `.github/workflows/crashlytics-autotriage.yml` | The triage workflow (runs on GitHub) |
| `.github/firebase-mcp.json` | Firebase MCP server config for the workflow |

## Secrets & access (where each one lives)

| Name | Lives in | Purpose | Expires? |
|------|----------|---------|----------|
| `GITHUB_TOKEN` | Firebase (Secret Manager) | Function files GitHub issues | **Yes — ~June 2027** (see above) |
| `ANTHROPIC_API_KEY` | GitHub repo Actions secrets | Runs Claude | No (unless rotated) |
| `GCP_SA_KEY` | GitHub repo Actions secrets | Lets the workflow read Crashlytics | No expiry, but key can be rotated |
| Service account `gh-crashlytics-reader` | Google Cloud IAM | Holds role `Firebase Crashlytics Viewer` | — |

## Common commands

```
firebase functions:list # is the function deployed?
firebase deploy --only functions:crashlytics-autotriage # (re)deploy
firebase functions:secrets:set GITHUB_TOKEN # set/rotate the token (paste at hidden prompt)
firebase functions:secrets:access GITHUB_TOKEN # confirm a value exists (does not print it)
```

To read function logs (e.g. to see a `401` from an expired token): Firebase Console → Functions → `crashlyticsToGithub` → Logs, or Google Cloud Console → Logging.

## Troubleshooting — in priority order

1. **No new GitHub issues from crashes?** → **Token expired** (most likely). Renew it (top of this doc). Then check the function logs for `401`.
2. **Function not firing at all?** → Confirm it's deployed (`firebase functions:list`) and the project is still on the **Blaze** plan.
3. **Issue created but no Claude comment / draft PR?** → Check the **Actions** tab. Likely a missing/expired `ANTHROPIC_API_KEY`, the Claude GitHub App was removed, or `GCP_SA_KEY` lost its Crashlytics access.
4. **Claude can't read the crash details?** → The Crashlytics MCP tools are **Experimental** and can change; also verify the service account still has `Firebase Crashlytics Viewer`.
5. **Duplicate issues for one crash?** → The function dedupes on the `Crashlytics-Issue-ID:` marker; if the issue body format changed, the dedupe search can miss.

## Notes

- Crashlytics MCP tooling is **Experimental** at Google (no SLA). The function is built so a crash still becomes a GitHub issue even if the MCP/detective half breaks.
- The bot only ever opens **draft** PRs and never merges — a human always reviews.
Loading