From 48c24f8e65e668447b116e20bc6556c0b2fa7bfa Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 08:27:43 -0400 Subject: [PATCH] chore(triage): add build-lookup and r8-mapping skills, wire into triage Add two new Claude Code skills: - build-lookup: resolves a versionCode to its git commit and GitHub Actions run - r8-mapping: downloads the R8 mapping file from CI artifacts for a given build Update the triage skill with Step 3a to automatically deobfuscate R8-obfuscated stack traces when Bugsnag fails to apply the mapping. Signed-off-by: Brandon McAnsh --- .claude/skills/build-lookup/SKILL.md | 64 +++++++++++ .../build-lookup/scripts/build-lookup.sh | 104 ++++++++++++++++++ .claude/skills/r8-mapping/SKILL.md | 97 ++++++++++++++++ .../skills/r8-mapping/scripts/r8-mapping.sh | 94 ++++++++++++++++ .claude/skills/triage/SKILL.md | 39 +++++++ 5 files changed, 398 insertions(+) create mode 100644 .claude/skills/build-lookup/SKILL.md create mode 100755 .claude/skills/build-lookup/scripts/build-lookup.sh create mode 100644 .claude/skills/r8-mapping/SKILL.md create mode 100755 .claude/skills/r8-mapping/scripts/r8-mapping.sh diff --git a/.claude/skills/build-lookup/SKILL.md b/.claude/skills/build-lookup/SKILL.md new file mode 100644 index 000000000..9cd82ba24 --- /dev/null +++ b/.claude/skills/build-lookup/SKILL.md @@ -0,0 +1,64 @@ +--- +name: build-lookup +description: > + Look up the git commit and GitHub Actions run for a Flipcash versionCode. + Usage: /build-lookup +user-invocable: true +argument-hint: "" +allowed-tools: + - Bash +--- + +# Build Lookup: versionCode to commit + CI run + +Resolve a Flipcash versionCode to its git commit and the associated GitHub +Actions build run. + +## Background + +The Flipcash versionCode is the output of `git rev-list --count HEAD` at build +time (see `apps/flipcash/app/build.gradle.kts:20-25`). This means +**versionCode N = the Nth commit from the root of the repository**. + +When someone says "commit 3797" or "build 3797" they mean versionCode 3797. + +## Usage + +Parse `$ARGUMENTS` for a versionCode (integer). If not provided, ask the user. + +Run the helper script: + +```bash +bash .claude/skills/build-lookup/scripts/build-lookup.sh +``` + +The script emits JSON: + +```json +{ + "version_code": 3797, + "commit": { + "sha": "637722b9c...", + "short": "637722b9c", + "message": "the commit message", + "date": "2026-06-03T..." + }, + "total_commits": 3810, + "github_actions": { + "run_id": 26846060877, + "url": "https://github.com/code-payments/code-android-app/actions/runs/26846060877", + "status": "completed", + "conclusion": "success" + } +} +``` + +`github_actions` is `null` if no matching workflow run was found. + +## Output + +Present the results clearly: + +- **versionCode**: N +- **Commit**: `` — `` (date) +- **GitHub Actions**: [Run #ID](url) — status/conclusion diff --git a/.claude/skills/build-lookup/scripts/build-lookup.sh b/.claude/skills/build-lookup/scripts/build-lookup.sh new file mode 100755 index 000000000..6d761f757 --- /dev/null +++ b/.claude/skills/build-lookup/scripts/build-lookup.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# build-lookup.sh — Resolve a versionCode to its git commit and GitHub Action run. +# +# The Flipcash versionCode is derived from `git rev-list --count HEAD` (see +# apps/flipcash/app/build.gradle.kts:20-25). versionCode N = the Nth commit +# from the root of the repo. +# +# Usage: +# ./build-lookup.sh +# ./build-lookup.sh 3797 +# +# Output: JSON with commit_sha, commit_short, commit_message, and (if found) +# the GitHub Actions run ID and URL for the Flipcash2 workflow. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: build-lookup.sh " >&2 + exit 1 +fi + +VERSION_CODE="$1" +WORKFLOW_ID="229420296" # "Flipcash2 Build and Deploy" + +# ── Resolve versionCode to commit ─────────────────────────────────── +# versionCode = git rev-list --count HEAD at the time of build. +# The Nth commit (1-indexed from root) is at position N in `git rev-list --reverse HEAD`. +# Equivalently: git rev-list HEAD | sed -n 'p' where offset = total - N + 1. + +TOTAL=$(git rev-list --count HEAD) + +if (( VERSION_CODE > TOTAL )); then + echo "ERROR: versionCode $VERSION_CODE exceeds current commit count ($TOTAL)" >&2 + exit 1 +fi + +if (( VERSION_CODE < 1 )); then + echo "ERROR: versionCode must be >= 1" >&2 + exit 1 +fi + +OFFSET=$(( TOTAL - VERSION_CODE + 1 )) +COMMIT_SHA=$(git rev-list HEAD | sed -n "${OFFSET}p") +COMMIT_SHORT="${COMMIT_SHA:0:9}" +COMMIT_MESSAGE=$(git log -1 --format='%s' "$COMMIT_SHA") +COMMIT_DATE=$(git log -1 --format='%aI' "$COMMIT_SHA") + +# ── Find GitHub Actions run for this commit ───────────────────────── +RUN_JSON="" +RUN_ID="" +RUN_URL="" +RUN_STATUS="" +RUN_CONCLUSION="" + +if command -v gh &>/dev/null; then + # gh run list --commit requires a full SHA match, so fetch recent runs and + # filter by headSha prefix instead. We pull the last 100 runs to cover ~2 + # weeks of builds. + ALL_RUNS=$(gh run list \ + --workflow="$WORKFLOW_ID" \ + --json databaseId,url,status,conclusion,headSha \ + --limit 100 2>/dev/null || true) + + if [[ -n "$ALL_RUNS" ]]; then + MATCH=$(echo "$ALL_RUNS" | jq --arg sha "$COMMIT_SHA" '[.[] | select(.headSha == $sha)] | .[0] // empty') + if [[ -n "$MATCH" ]]; then + RUN_ID=$(echo "$MATCH" | jq -r '.databaseId') + RUN_URL=$(echo "$MATCH" | jq -r '.url') + RUN_STATUS=$(echo "$MATCH" | jq -r '.status') + RUN_CONCLUSION=$(echo "$MATCH" | jq -r '.conclusion') + fi + fi +fi + +# ── Emit result ───────────────────────────────────────────────────── +jq -n \ + --arg version_code "$VERSION_CODE" \ + --arg commit_sha "$COMMIT_SHA" \ + --arg commit_short "$COMMIT_SHORT" \ + --arg commit_message "$COMMIT_MESSAGE" \ + --arg commit_date "$COMMIT_DATE" \ + --arg total_commits "$TOTAL" \ + --arg run_id "${RUN_ID:-}" \ + --arg run_url "${RUN_URL:-}" \ + --arg run_status "${RUN_STATUS:-}" \ + --arg run_conclusion "${RUN_CONCLUSION:-}" \ + '{ + version_code: ($version_code | tonumber), + commit: { + sha: $commit_sha, + short: $commit_short, + message: $commit_message, + date: $commit_date + }, + total_commits: ($total_commits | tonumber), + github_actions: ( + if $run_id != "" then { + run_id: ($run_id | tonumber), + url: $run_url, + status: $run_status, + conclusion: $run_conclusion + } else null end + ) + }' diff --git a/.claude/skills/r8-mapping/SKILL.md b/.claude/skills/r8-mapping/SKILL.md new file mode 100644 index 000000000..2473871a4 --- /dev/null +++ b/.claude/skills/r8-mapping/SKILL.md @@ -0,0 +1,97 @@ +--- +name: r8-mapping +description: > + Download and use the R8 mapping file for a Flipcash release build to + deobfuscate stack traces. Usage: /r8-mapping +user-invocable: true +argument-hint: "" +allowed-tools: + - Bash + - Read + - Grep +--- + +# R8 Mapping: download and deobfuscate + +Download the R8 `mapping.txt` for a Flipcash release build and use it to +deobfuscate stack traces. + +## Background + +Release builds are minified/obfuscated by R8. The mapping file is uploaded as +part of the `release-artifacts` artifact in the "Flipcash2 Build and Deploy" +GitHub Actions workflow (ID `229420296`). The mapping lives at +`mapping/release/mapping.txt` inside the artifact. + +**R8 class merging**: R8 may merge multiple unrelated classes into one +obfuscated class. Use line numbers from the stack trace to disambiguate which +original class a frame belongs to. + +## Step 1 — Download the mapping + +Parse `$ARGUMENTS` for a versionCode (integer). If not provided, ask the user. + +```bash +bash .claude/skills/r8-mapping/scripts/r8-mapping.sh +``` + +The script emits JSON: + +```json +{ + "mapping_path": "/tmp/r8-mapping-3797/mapping/release/mapping.txt", + "run_id": 26846060877, + "version_code": 3797, + "line_count": 1211075 +} +``` + +## Step 2 — Deobfuscate classes + +To find what an obfuscated class name maps to: + +```bash +grep " -> :" +``` + +This returns lines like: +``` +com.original.ClassName -> ag3: +com.other.MergedClass -> ag3: +``` + +Multiple results means R8 merged those classes. Use stack trace line numbers +to disambiguate — each class section in the mapping contains line-number +ranges for its methods. + +## Step 3 — Deobfuscate methods + +After finding the class section, look for method mappings within it: + +```bash +# Find the class section and its method mappings +grep -A 200 "^com.original.ClassName -> :" | head -200 +``` + +Method lines look like: +``` + 1:5:void run():123:127 -> run + 6:10:void otherMethod():45:49 -> a +``` + +The format is: +``` + :: ():: -> +``` + +Match the line number from the stack trace against `obfuscated_line_start:obfuscated_line_end` +to find the original method and its original line numbers. + +## Output + +Present deobfuscated stack frames clearly: + +``` +Original: com.original.ClassName.methodName (ClassName.kt:127) + Obfuscated: ag3.run (SourceFile:3) +``` diff --git a/.claude/skills/r8-mapping/scripts/r8-mapping.sh b/.claude/skills/r8-mapping/scripts/r8-mapping.sh new file mode 100755 index 000000000..6ce2434a3 --- /dev/null +++ b/.claude/skills/r8-mapping/scripts/r8-mapping.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# r8-mapping.sh — Download the R8 mapping file for a Flipcash release build. +# +# Downloads the `release-artifacts` artifact from the GitHub Actions run +# associated with a given versionCode, then extracts the R8 mapping.txt. +# +# Usage: +# ./r8-mapping.sh +# ./r8-mapping.sh --run-id +# +# Output: JSON with the path to the downloaded mapping file and metadata. +# The mapping file is saved to /tmp/r8-mapping-/mapping/release/mapping.txt + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +BUILD_LOOKUP="$REPO_ROOT/.claude/skills/build-lookup/scripts/build-lookup.sh" +WORKFLOW_ID="229420296" + +# ── Argument parsing ──────────────────────────────────────────────── +RUN_ID="" +VERSION_CODE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --run-id) RUN_ID="$2"; shift 2 ;; + *) VERSION_CODE="$1"; shift ;; + esac +done + +# ── Resolve run ID if only versionCode given ──────────────────────── +if [[ -z "$RUN_ID" && -n "$VERSION_CODE" ]]; then + LOOKUP_JSON=$(bash "$BUILD_LOOKUP" "$VERSION_CODE") + + RUN_ID=$(echo "$LOOKUP_JSON" | jq -r '.github_actions.run_id // empty') + COMMIT_SHA=$(echo "$LOOKUP_JSON" | jq -r '.commit.sha') + + if [[ -z "$RUN_ID" ]]; then + echo "ERROR: No GitHub Actions run found for versionCode $VERSION_CODE (commit $COMMIT_SHA)" >&2 + echo "Try: gh run list --workflow=$WORKFLOW_ID --commit=${COMMIT_SHA:0:9} --limit=10" >&2 + exit 1 + fi +elif [[ -z "$RUN_ID" ]]; then + echo "Usage: r8-mapping.sh " >&2 + echo " r8-mapping.sh --run-id " >&2 + exit 1 +fi + +# ── Download artifact ─────────────────────────────────────────────── +DEST_DIR="/tmp/r8-mapping-${VERSION_CODE:-$RUN_ID}" +MAPPING_PATH="$DEST_DIR/mapping/release/mapping.txt" + +if [[ -f "$MAPPING_PATH" ]]; then + # Already downloaded, skip + LINE_COUNT=$(wc -l < "$MAPPING_PATH" | tr -d ' ') +else + rm -rf "$DEST_DIR" + mkdir -p "$DEST_DIR" + + echo "Downloading release-artifacts from run $RUN_ID..." >&2 + gh run download "$RUN_ID" \ + -n release-artifacts \ + -D "$DEST_DIR" 2>&1 >&2 + + if [[ ! -f "$MAPPING_PATH" ]]; then + # Try alternate path — artifact may have different structure + ALT=$(find "$DEST_DIR" -name "mapping.txt" -type f 2>/dev/null | head -1) + if [[ -n "$ALT" ]]; then + MAPPING_PATH="$ALT" + else + echo "ERROR: mapping.txt not found in downloaded artifacts" >&2 + echo "Contents of $DEST_DIR:" >&2 + find "$DEST_DIR" -type f >&2 + exit 1 + fi + fi + + LINE_COUNT=$(wc -l < "$MAPPING_PATH" | tr -d ' ') + echo "Downloaded mapping.txt ($LINE_COUNT lines)" >&2 +fi + +# ── Emit result ───────────────────────────────────────────────────── +jq -n \ + --arg mapping_path "$MAPPING_PATH" \ + --arg run_id "$RUN_ID" \ + --arg version_code "${VERSION_CODE:-unknown}" \ + --arg line_count "$LINE_COUNT" \ + '{ + mapping_path: $mapping_path, + run_id: ($run_id | tonumber), + version_code: (if $version_code != "unknown" then ($version_code | tonumber) else null end), + line_count: ($line_count | tonumber) + }' diff --git a/.claude/skills/triage/SKILL.md b/.claude/skills/triage/SKILL.md index 2f0cb1e89..5a099cc7e 100644 --- a/.claude/skills/triage/SKILL.md +++ b/.claude/skills/triage/SKILL.md @@ -86,6 +86,45 @@ Android stack frames use Java/Kotlin package-qualified class names (e.g. Only map frames where `inProject` is `true`. +### 3a. Deobfuscate R8-obfuscated stacks + +If the stack trace contains obfuscated class/method names (short lowercase names +like `ag3`, `b0`, `x2.a`, or `SourceFile` instead of real filenames), Bugsnag +failed to apply the R8 mapping. Deobfuscate manually: + +1. **Identify the versionCode** from the event's `app.versionCode` field. If + only `versionName` is available, check `.well-known/release-manifest.json` + for the corresponding versionCode. + +2. **Download the R8 mapping** using the `r8-mapping` skill: + ```bash + bash .claude/skills/r8-mapping/scripts/r8-mapping.sh + ``` + This returns JSON with `mapping_path` pointing to the local mapping file. + +3. **Look up obfuscated classes**: + ```bash + grep " -> :" + ``` + Multiple results indicate R8 class merging — use stack trace line numbers + to disambiguate (each class section contains line-number ranges for methods). + +4. **Look up methods within a class section**: + ```bash + grep -A 200 "^com.original.ClassName -> :" | head -200 + ``` + Match the stack frame's line number against `obfuscated_line_start:obfuscated_line_end` + to find the original method and source line. + +5. **Replace** obfuscated frames with deobfuscated ones before proceeding to + Step 4. + +If the `r8-mapping` script needs a GitHub Actions run ID instead: +```bash +bash .claude/skills/build-lookup/scripts/build-lookup.sh +``` +to find the run, then pass `--run-id ` to the r8-mapping script. + ## Step 4 — Build the evidence timeline ### 4a. Version check