Operational doc · 2026-05-08

TestFlight pipeline

How Frank's Fed builds get to testers, how tester comments get back to us, and how UAT closes the loop. Both directions are now wired up — this page is the operating manual.

deployment testflight two-way feedback first deploy pending walkthrough

The shape

Two Claude Code skills, one for each direction. Both share the same App Store Connect API key and the same git repo state, so there's only one copy of credentials, one source of truth for build numbers, and one place to look when something goes wrong.

DirectionSkillWhat it does
Out /testflight Bumps the build number, asks Claude Haiku to write voice-aware release notes from the recent git commits, archives + uploads to App Store Connect, then posts the "What's New" text and verifies it read-back. Tags the release.
In /franks-fed-pull-feedback Pulls TestFlight tester comments + screenshots + crash logs from App Store Connect into .planning/inbox/testflight-feedback.md as triage items. Idempotent — re-runs skip already-seen submissions.

Outbound — when we ship a build

Git commits since last testflight/* tag Claude Haiku voice-aware release notes xcodebuild archive SPM build → build/archive.log App Store Connect upload + PATCH betaBuildLocalization Apple processes ~10–30 minutes TestFlight build available to testers Git tag testflight/vX-build-N summarise build upload distribute All of the above runs from one command: /testflight Build bump, notes, archive, upload, post-and-verify, tag — five-to-ten minutes elapsed, ~10–30 min more for Apple processing.
Outbound flow — from git commit to tester. Solid arrows = automation; dashed footer = the single command that runs the whole sequence.

What ends up in the tester's hands

Voice constraint, in case it matters

The Claude Haiku prompt enforces Frank's Fed voice (Bible §13). If a deploy ever posts notes that contain a banned word, that's a regression worth tightening — the notes go through ASC API rejection of emoji, but not banned-word checking. Open improvement; the hand-written fallback notes (used when Haiku returns empty) are pre-vetted.

Inbound — when testers send something back

Tester screenshot + comment or crash report App Store Connect betaFeedback* Submissions (JWT-signed reads) Inbox .planning/inbox/ testflight-feedback.md deduped, append-only Triage human reads, decides: phase / backlog / dismiss 3-checkbox checklist Phase HUMAN-UAT.md .planning/phases/{N}/{NN}-HUMAN-UAT.md submission_id kept in entry header → surfaces on the UAT board Fix commit code change ships, tester re-tests on next build verification commit /gsd-close-uat matches submission_id, marks the VERIFICATION.md gap resolved, commits UAT board updates → publish skill regenerates the live site → done. routed to phase /gsd-debug in-app feedback /franks-fed- pull-feedback human read
Inbound flow — from tester comment all the way back to a closed UAT entry. Green arrow = the closure step that updates the public board.

The state file is your dedupe

Every pulled submission ID lives in .planning/inbox/.testflight-feedback-state.json alongside last_pulled_at. Re-running /franks-fed-pull-feedback picks up where you left off. The state file is committed — so dedupe survives across dev machines.

Why no auto-classification

Whether a tester comment belongs to Phase 5 (Memorial dignity), Phase 4 (helper invite friction), or Phase 6 (a brand-new bug) is a judgement call that needs context the API doesn't give us. The inbox is a holding queue. A future /gsd-triage-feedback skill could batch-classify with Haiku, but for the closed-beta cohort the manual triage is the right shape — it's also the moment the founder reads each comment in full, which we don't want to skip.

Working it next week

Phase 5 walkthrough on TestFlight — what to do

Phase 5 (Memorial UI + Safe-to-Walk + Voice/A11y polish + Notifications) needs your real-device verification before it can mark verified. Here's the sequence:

  1. Cut the build: from the Frank's Fed repo working directory, run /testflight. The skill takes ~5–10 minutes; Apple takes ~10–30 more to process. You'll get an email when it's ready in TestFlight.
  2. Install on your iPhone: open TestFlight on the device, accept the new build, install. It supersedes whatever was there.
  3. Run the five a11y manual passes against the Phase 5 surfaces (Memorial UI, Safe-to-Walk badge, Notifications settings, Help screen, mixed-household status). For each pass:
    • VoiceOver tour: Settings → Accessibility → VoiceOver ON. Walk every Phase 5 surface. Confirm every announcement is in Bible voice ("Frank is fed and resting", not "feed-status: complete").
    • Dynamic Type at xxxLarge: Settings → Display & Text Size → max. Confirm no truncation or overlap.
    • Reduce Motion: Settings → Accessibility → Motion → ON. Confirm Memorial sheets, log toasts, Help DisclosureGroups all use instant transitions.
    • WCAG AA contrast: spot-check the eight colour pairs (Frank/Miles body on dominant, primary on white, status tints) with WebAIM/Stark/APCA.
    • 44pt touch targets: Xcode Accessibility Inspector → Audit → run against the TestFlight build. Resolve any "small touch target" findings on Phase 5 surfaces.
    Record findings inline in .planning/phases/05-memories-safe-to-walk-voice-a11y-notifications/05-A11Y-AUDIT.md.
  4. Run the "pretend a pet died" walkthrough: open the app as your own household. Read every Memorial-flow screen aloud. Tap "Move to Memories", watch the toast, navigate to Settings → Memories → MemorialPetView. Confirm: pet's photo + name preserved, no log buttons, chip says "In Memories since [month]", HomeView add-pet affordance is silently absent, hard-delete is disabled with the calm helper copy. Run the same flow in the other voice variant. Score against V8 §13's five dignity principles.
  5. Capture the verdict: fill in .planning/phases/05-memories-safe-to-walk-voice-a11y-notifications/05-HUMAN-UAT.md. If you find any dignity-violating copy or bad VoiceOver labels, list them as gaps with the exact surface + suggested fix.
  6. Reply to Claude with your verdict. Three options:
    • approved — both the a11y audit and walkthrough are clean (or you're shipping the fixes inline before signing off). Plan 09 Task 3 fires, the verifier runs, Phase 5 closes.
    • blocking issues: <list> — describe each gap. Claude opens fix tasks before Phase 6 unblocks.
    • skip walkthrough — not recommended. The walkthrough is the only end-to-end dignity test before real pet owners use the product.

If you also have testers running this build

Run /franks-fed-pull-feedback after a few testers have had the build for a day or two. New comments + screenshots land in .planning/inbox/testflight-feedback.md with a triage checklist. For each one, decide:

  1. Reviewed — read it, looked at the screenshot, understood what they saw.
  2. Routed — if it's a real verification gap in the current phase, copy it into that phase's HUMAN-UAT.md as a new human_verification entry, keeping the submission ID in the header (e.g. submission_id: abc12345). Or file it on BACKLOG.md. Or mark won't-fix.
  3. Resolved — once the fix lands or the won't-fix decision is recorded, tick it.

The /gsd-close-uat skill keys on submission IDs to mark items resolved when their fix commits land. The UAT board on this site updates automatically on the next publish.

Where the bits live

File / pathPurpose
scripts/deploy-testflight.shOutbound deploy + Haiku notes + post-and-verify "What's New"
scripts/submit-whats-new.shJWT signing + ASC API polling + betaBuildLocalization create/PATCH/verify
scripts/pull-tester-feedback.shInbound pull of betaFeedbackScreenshotSubmissions + betaFeedbackCrashSubmissions
scripts/ExportOptions.plistmethod=app-store-connect, destination=upload, Team DF3ZPT9XFN
.claude/skills/testflight/SKILL.mdProject-scoped /testflight skill (overrides InfoVoice's user-level skill in this repo)
.claude/skills/franks-fed-pull-feedback/SKILL.md/franks-fed-pull-feedback skill
private_keys/AuthKey_*.p8App Store Connect API key — gitignored. Falls back to ~/private_keys/ if the project dir doesn't have one.
build/whats-new.mdGenerated release notes, latest deploy
build/archive.logFull xcodebuild output — first place to look on archive failure
.planning/inbox/testflight-feedback.mdInbound tester comments + screenshots + crashes (append-only, deduped)
.planning/inbox/.testflight-feedback-state.jsonDedup state — seen_ids[] + last_pulled_at
Docs/testflight-deployment.mdIn-repo operational reference (longer than this page)

One-time gates before the first deploy

None of this works until the Apple side is registered. Set these up in Apple Developer Portal + App Store Connect, in this order:

The skills will fail loudly with an actionable error message if any of these is missing on first run. scripts/submit-whats-new.sh's Could not look up App ID for be.780.franks-fed is the most common one — that's the App Store Connect record not being created yet.

Credentials, in one place

FieldValue
Issuer IDffbb8a56-2513-4d2e-b6cc-d6dbbe94762f
Primary Key IDJR5N75FS49
Fallback Key ID9F5BTGY6J2
Bundle IDbe.780.franks-fed
Team IDDF3ZPT9XFN

Same Apple developer account, same Issuer, same .p8 keys as InfoVoice. The script looks in the project's private_keys/ first, then falls back to ~/private_keys/ — so cloning the repo on a new machine doesn't strictly require copying keys into the project, as long as you have them in your home dir.