Skip to content

fix(cli/build init): don't re-ask saved import-flow answers on resume#2309

Merged
WcaleNieWolny merged 2 commits into
mainfrom
fix/cli-import-resume-skip-asked
May 21, 2026
Merged

fix(cli/build init): don't re-ask saved import-flow answers on resume#2309
WcaleNieWolny merged 2 commits into
mainfrom
fix/cli-import-resume-skip-asked

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented May 21, 2026

Summary

A resumed build init --platform ios import flow was asking the user again for their distribution mode AND for their .p8 file path, even when both were already saved in the per-app onboarding progress file. The most visible symptom: the screen showed "✔ API Key verified — Key: 66FGQZB566" (hydrated log, replayed from progress) sitting right next to "How do you want to provide the .p8 file?" (the prompt that has no business being there).

Concrete trace against a real ~/.capgo-credentials/onboarding/<appId>.json:

{
  "setupMethod": "import-existing",
  "importDistribution": "app_store",
  "completedSteps": { "apiKeyVerified": { "keyId": "", "issuerId": "" } },
  "p8Path": "/path/to/AuthKey_….p8",
  "keyId": "", "issuerId": ""
}
  1. getResumeStep returned 'import-scanning' (correct).
  2. import-scanning useEffect ran the Keychain scan, then unconditionally setStep('import-distribution-mode')ignoring importDistribution: "app_store" in progress.
  3. The distribution-mode Select's app_store branch unconditionally setStep('api-key-instructions')ignoring the saved key inputs.

So the user re-picked distribution, then was thrown to the .p8 file picker even though APPLE_KEY_CONTENT and friends were already known.

Fix

New getImportEntryStep(progress) helper in progress.ts centralizes the resume-routing decision after a successful Keychain scan:

Progress state Next step
no progress / no importDistribution import-distribution-mode
importDistribution: ad_hoc import-pick-identity
app_store + .p8 + keyId + issuerId verifying-key
app_store + .p8 + keyId input-issuer-id
app_store + .p8 input-key-id
app_store + nothing api-key-instructions

Both call sites now use the helper:

  • import-scanning useEffect: setStep(getImportEntryStep(await loadProgress(appId)))
  • distribution-mode app_store branch: setStep(getImportEntryStep(existing)) (uses the just-saved progress so a re-picked mode still flows correctly)

Why we don't short-circuit on apiKeyVerified

An earlier version of this PR routed apiKeyVerified resumes directly to import-pick-identity, skipping verifying-key. Code review correctly flagged this as a regression: the saved key material could have gone stale (the .p8 file moved/deleted between runs, or the key revoked on Apple's side), and a short-circuit would only surface that failure inside saving-credentialsafter identity selection, profile selection, and the Keychain ACL prompt that fires during the .p12 export. A malformed progress file with no p8Path could even silently save credentials missing APPLE_KEY_* entirely.

Going through verifying-key on every resume is a ~500ms Apple round-trip that catches both failure modes early. The user still doesn't see the .p8 picker UI on resume (which was the original bug) — they see a brief spinner, then land at the identity picker.

Three defense-in-depth changes back this up:

  1. getImportEntryStep routes (app_store + .p8 + keyId + issuerId) to verifying-key, regardless of apiKeyVerified.
  2. getFreshToken wraps readFile in try/catch and rethrows as NeedP8Error. handleError already routes NeedP8Error back to api-key-instructions, so a stale path produces a clean re-prompt instead of an ENOENT-decorated support-bundle screen.
  3. doSaveCredentials adds a defensive guard: throws when needsAscKey && !keyContent after the recovery read. Prevents the silent save-without-APPLE_KEY_* path on malformed progress files.

Test plan

  • bun run typecheck — clean
  • bunx eslint on touched files — only pre-existing main errors remain, none new
  • New: bun test/test-onboarding-progress.mjs — 12 tests covering the full decision table, including a test case shaped exactly like the real-world bug report (asserts verifying-key, not import-pick-identity)
  • bun test/test-onboarding-recovery.mjs — all pass
  • bun test/test-macos-signing.mjs — all pass
  • bun run build — clean
  • Manual: with the bug-shaped progress file present at ~/.capgo-credentials/onboarding/<appId>.json (apiKeyVerified set, importDistribution: app_store), re-run build init --platform ios and verify the flow lands on verifying-key (brief spinner) then import-pick-identity — no distribution-mode picker, no .p8 file picker
  • Manual: same as above, but with the saved .p8 moved/deleted — verify the user gets routed back to api-key-instructions for a clean re-prompt (NeedP8Error path), not a support-bundle error screen
  • Manual: with importDistribution: ad_hoc and nothing else, verify direct landing on import-pick-identity
  • Manual: fresh run (no progress file) — verify distribution-mode picker still shows first as before

Related

Summary by CodeRabbit

  • New Features
    • Import flow now resumes from saved progress, letting users continue onboarding where they left off.
  • Bug Fixes
    • Improved handling of missing/invalid private key files so the UI prompts for re-entry and prevents saving empty credentials.
  • Tests
    • Added comprehensive tests covering onboarding progress step selection and flow resumption scenarios.
  • Chores
    • Added a dedicated test command to run the onboarding progress test suite.

Review Change Stack

A resumed `build init --platform ios` import flow was asking the user
again for their distribution mode AND for their .p8 file path, even
when both were already saved in the per-app onboarding progress file —
producing screens like "✔ API Key verified — Key: 66FGQZB566" (hydrated
log from progress) sitting right next to "How do you want to provide
the .p8 file?" (the prompt that has no business being there).

Two unconditional setStep() calls were responsible:

1. `import-scanning` useEffect always routed to
   `import-distribution-mode` regardless of `progress.importDistribution`.
2. The `import-distribution-mode` Select's app_store branch always
   routed to `api-key-instructions`, regardless of
   `progress.completedSteps.apiKeyVerified`.

Both now consult the new `getImportEntryStep` helper in progress.ts,
which centralizes the resume-routing decision:

- no progress / no importDistribution        → import-distribution-mode
- importDistribution = ad_hoc                → import-pick-identity
- app_store + apiKeyVerified                 → import-pick-identity
- app_store + .p8 + keyId + issuerId         → verifying-key
- app_store + .p8 + keyId                    → input-issuer-id
- app_store + .p8                            → input-key-id
- app_store + nothing                        → api-key-instructions

12 unit tests cover the full decision table including the exact
real-world bug shape (apiKeyVerified set, .p8 already on disk, was
being re-asked).

No change to `getResumeStep` itself or to first-run behavior —
`getImportEntryStep` only fires after a successful Keychain scan.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f624b1f-8207-48f4-9d1e-e16dbdd4dd71

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8a7b8 and 7ee892c.

📒 Files selected for processing (3)
  • cli/src/build/onboarding/progress.ts
  • cli/src/build/onboarding/ui/app.tsx
  • cli/test/test-onboarding-progress.mjs

📝 Walkthrough

Walkthrough

This PR centralizes onboarding step routing for iOS imports into a pure helper, integrates it into the UI so resumed sessions skip already-completed import steps, adds defensive .p8 error handling and validation, and introduces unit tests plus a package.json test script.

Changes

Resume-aware onboarding step routing

Layer / File(s) Summary
Import entry step routing function
cli/src/build/onboarding/progress.ts
New getImportEntryStep exported helper determines the post-scan onboarding step: returns import-distribution-mode when no prior distribution exists, maps ad_hoc directly to identity picking, and for app_store resumes at the correct .p8/issuer/key input or verification step based on persisted fields.
UI integration and defensive .p8 handling
cli/src/build/onboarding/ui/app.tsx
Replaces fixed next-step transitions with getImportEntryStep(await loadProgress(appId)) in import-scanning and getImportEntryStep(existing) in import-distribution-mode for app_store; wraps .p8 reads to throw NeedP8Error and prevents saving credentials when required .p8 content is missing.
Tests and package script
cli/test/test-onboarding-progress.mjs, cli/package.json
Adds tests covering getImportEntryStep scenarios (null/empty, ad_hoc, app_store with missing/partial .p8/key/issuer, regressions for getResumeStep) and registers test:onboarding-progress in package.json; included in the composite test script.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Cap-go/capgo#2211: Both PRs modify the iOS "import existing" onboarding flow in progress.ts and ui/app.tsx to control step resumption; this PR adds centralized getImportEntryStep routing logic while the related PR implements broader import-existing flow changes.

Poem

A rabbit hops through screens with care,
Remembers keys and places there—
No needless steps, no looping chore,
Resume the path you walked before,
Small paws, clear progress, onward fare. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preventing re-prompting for saved import-flow answers (distribution mode and .p8 file) during resume operations.
Description check ✅ Passed The description is comprehensive and well-structured, including Summary, concrete issue trace, Fix explanation with decision table, reasoning for design choices, detailed Test plan with manual verification steps, and Related links.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-import-resume-skip-asked

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 21, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix/cli-import-resume-skip-asked (7ee892c) with main (ebf5e04)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b8a7b8ca0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/build/onboarding/progress.ts Outdated
Comment on lines +168 to +169
if (progress.completedSteps.apiKeyVerified)
return 'import-pick-identity'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate saved ASC key before skipping .p8 flow

This branch skips the entire App Store Connect key path whenever apiKeyVerified is present, but it does not verify that the saved key material is still usable (for example, the .p8 file was moved/deleted between runs). In that resume scenario, the user is routed straight to identity/profile selection and only fails later in saving-credentials (or can even save without APPLE_KEY_* if no path is present), with no step that lets them refresh the key path. Before this change, app_store always re-entered the key flow and recovered from stale local paths.

Useful? React with 👍 / 👎.

…sting apiKeyVerified

Code-review caught a real regression: the previous commit's
apiKeyVerified short-circuit routed users straight from import-scanning
to import-pick-identity, skipping verifying-key. If the saved .p8 file
had been moved/deleted (or the key revoked on Apple's side) between
runs, the user would only fail later inside saving-credentials — after
identity selection, profile selection, AND the Keychain ACL prompt that
fires during the .p12 export. Worse: a malformed progress file with no
p8Path could silently save credentials missing APPLE_KEY_* entirely.

Three fixes:

1. getImportEntryStep no longer short-circuits on apiKeyVerified. When
   all three inputs (p8Path / keyId / issuerId) are saved we go to
   verifying-key — a ~500ms Apple round-trip that catches both stale
   local files and Apple-side revocation, then routes to
   import-pick-identity on success. The user still doesn't see the .p8
   picker UI on resume (which was the original bug).

2. getFreshToken now wraps readFile in try/catch and rethrows as
   NeedP8Error. handleError already routes NeedP8Error back to
   api-key-instructions, so a stale path produces a clean re-prompt
   instead of an ENOENT-decorated support-bundle screen.

3. doSaveCredentials adds a defensive guard: throws when needsAscKey
   but keyContent is empty after the recovery read. Prevents the silent
   save-without-APPLE_KEY_* path on malformed progress files.

Updated test names + the bug-shape test to assert verifying-key (not
import-pick-identity) as the expected resume target.
@sonarqubecloud
Copy link
Copy Markdown

@WcaleNieWolny WcaleNieWolny merged commit 648a5df into main May 21, 2026
44 checks passed
@WcaleNieWolny WcaleNieWolny deleted the fix/cli-import-resume-skip-asked branch May 21, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant