feat(crashes): add opdev crashes subcommand for iOS crash diagnosis - #22
Merged
Conversation
Ports ops/asc_client.py from the ownpulse repo into native Go so crash triage lives in opdev itself. Three subcommands: - `opdev crashes diagnose` — end-to-end report - `opdev crashes list-builds` — raw build list as JSON - `opdev crashes crash-feedback` — per-build diagnostics as JSON Implementation notes: - ES256 JWT minted with stdlib only (raw R||S, no third-party JWT lib) - HTTPGetter is injected so tests never hit Apple - links.next pagination asserts scheme=https and host=api.appstoreconnect.apple.com - SOPS is invoked via `sops -d <path>`; PEM never written to disk - 12 unit tests mirror the Python suite Adds gopkg.in/yaml.v3 as a direct dep.
- Remove --key-pem flag (PEM via env or SOPS only; prior flag exposed contents to ps/shell history). - Fix LoadKey to surface both PKCS8 and SEC1 parse errors. - Field-by-field credential precedence with new test coverage. - Disable HTTP redirect following; add host-pin tests for suffix, userinfo, mixed-case, and IP-literal vectors. - Redact Bearer tokens from ASCError.Body. - Tests for CrashFeedback 404-swallow and Diagnose signal filter. - %w error wrapping in three places; trailing-Z date parse fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The decrypted SOPS file uses asc_api_key_b64 (base64-encoded .p8 contents), not the key_pem field opdev was previously looking for. LoadCredentials now reads asc_api_key_b64 first, base64-decodes into PEM bytes, and falls back to key_pem if present. Tests cover both branches and the precedence ordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tonyferrell
marked this pull request as ready for review
May 25, 2026 18:15
opdev already knows where ownpulse-infra lives via workspace.toml. Stop requiring OWNPULSE_INFRA_PATH for the default case. Resolution order is now: 1. --sops-path flag (explicit) 2. OWNPULSE_INFRA_PATH env var (kept for non-workspace use) 3. Workspace config: ownpulse-infra repo + standard secrets path 4. ASC_* env vars (one-off override) The crashes package stays workspace-config-agnostic; the lookup lives in main.go alongside the existing loadConfig() helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
opdev crashes diagnose— pulls symbolicated iOS crash reports from App Store Connect so any Claude session (or a human at a terminal) can investigate user-reported crashes with one command.This is the Go-native landing place for what started as a Python script in ownpulse#247. That PR is now docs-only and points here.
Usage
Subcommands:
opdev crashes diagnose— primary surface. Resolves credentials, mints JWT, lists recent builds, fetches crash feedback per build, applies signal/build filters, renders table or--json.opdev crashes list-builds— debug helper. Just enumerates builds visible to this key.opdev crashes crash-feedback --build <id>— debug helper. Raw output for one build.Credentials resolve field-by-field in this order: explicit flag → env var → SOPS-loaded value. SOPS file lives at
$OWNPULSE_INFRA_PATH/secrets/ios/appstore-connect.sops.yamland supports bothasc_api_key_b64(production schema, base64-encoded.p8) andkey_pem(literal PEM, for backward compatibility / tests).What's in the package
src/crashes/crashes.go:crypto/ecdsa,crypto/x509,encoding/pem,encoding/base64). Raw R||S signature with 32-byte zero-padding, NOT DER. Hardcodedalg=ES256—alg=noneis unreachable. 20-minute expiry.assertASCHosthost pinning: requiresscheme=httpsand host exactlyapi.appstoreconnect.apple.com(case-insensitive). Called before every request, including the first.DefaultHTTPGetterdisables redirect following (http.ErrUseLastResponse) — Bearer token cannot leak via 302.ASCError.Bodyruns throughredactBearerbefore storage, so any echoedAuthorizationheader is scrubbed.LoadCredentialsshells out tosops -dand parses YAML in-process. PEM bytes never written to disk.src/crashes/crashes_test.go— 21 unit tests:sincefilter, pagination vialinks.next, off-host rejection, http-scheme rejection.TestAssertASCHost_Vectors— 14 sub-cases (suffix, prefix, userinfo, mixed case, IP literal, file scheme, empty, garbage).TestResolveCredentials_FieldByFieldPrecedence— 6 sub-cases covering flag/env/SOPS merging.TestDefaultHTTPGetter_DoesNotFollowRedirects— strong negative check.TestRedactBearer,TestCrashFeedback_Swallows404,TestCrashFeedback_PropagatesNon404,TestDiagnose_SignalFilter.TestLoadCredentials_SOPS_Base64EncodedandTestLoadCredentials_SOPS_PrefersBase64OverPEM— covers production schema.src/main.go—crashesCmd()registers the three subcommands.--key-pemis deliberately NOT a flag (would leak PEM viaps/ shell history).Reviews
LoadKeynow surfaces both PKCS8 and SEC1 parse errors,%werror wrapping in three places, trailing-Zdate parsing fixed.--key-pemCLI flag exposing PEM viaps) was the trigger for removing the flag entirely. Prior Medium findings (redirect following, host-pin test coverage, all-or-nothing credential precedence, Bearer-token leakage) all resolved.Dependencies
gopkg.in/yaml.v3(direct).Verification
End-to-end verification (requires the SOPS file in ownpulse-infra to have
app_idpopulated — separate one-line infra PR coming):Out of scope
/admin/telemetry/crashesendpoint, first-launch consent prompt, offline retry queue) — separate PRs in the main ownpulse repo.ownpulse-infra/.github/workflows/ios-build.yml(manual dispatch). Triggering on tag push + verifying dSYM upload is a small follow-up.download_dsymsstub returnsNotImplementedError; can fill in if Apple-side reports come back partially symbolicated.🤖 Generated with Claude Code