@zigrivers/scaffold 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -12
- package/content/knowledge/backend/backend-api-design.md +103 -0
- package/content/knowledge/backend/backend-architecture.md +100 -0
- package/content/knowledge/backend/backend-async-patterns.md +101 -0
- package/content/knowledge/backend/backend-auth-patterns.md +100 -0
- package/content/knowledge/backend/backend-conventions.md +105 -0
- package/content/knowledge/backend/backend-data-modeling.md +102 -0
- package/content/knowledge/backend/backend-deployment.md +100 -0
- package/content/knowledge/backend/backend-dev-environment.md +102 -0
- package/content/knowledge/backend/backend-observability.md +102 -0
- package/content/knowledge/backend/backend-project-structure.md +100 -0
- package/content/knowledge/backend/backend-requirements.md +103 -0
- package/content/knowledge/backend/backend-security.md +104 -0
- package/content/knowledge/backend/backend-testing.md +101 -0
- package/content/knowledge/backend/backend-worker-patterns.md +100 -0
- package/content/knowledge/cli/cli-architecture.md +101 -0
- package/content/knowledge/cli/cli-conventions.md +117 -0
- package/content/knowledge/cli/cli-dev-environment.md +121 -0
- package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
- package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
- package/content/knowledge/cli/cli-output-patterns.md +107 -0
- package/content/knowledge/cli/cli-project-structure.md +124 -0
- package/content/knowledge/cli/cli-requirements.md +101 -0
- package/content/knowledge/cli/cli-shell-integration.md +130 -0
- package/content/knowledge/cli/cli-testing.md +134 -0
- package/content/knowledge/library/library-api-design.md +306 -0
- package/content/knowledge/library/library-architecture.md +247 -0
- package/content/knowledge/library/library-bundling.md +244 -0
- package/content/knowledge/library/library-conventions.md +229 -0
- package/content/knowledge/library/library-dev-environment.md +220 -0
- package/content/knowledge/library/library-documentation.md +300 -0
- package/content/knowledge/library/library-project-structure.md +237 -0
- package/content/knowledge/library/library-requirements.md +173 -0
- package/content/knowledge/library/library-security.md +257 -0
- package/content/knowledge/library/library-testing.md +319 -0
- package/content/knowledge/library/library-type-definitions.md +284 -0
- package/content/knowledge/library/library-versioning.md +300 -0
- package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
- package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
- package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
- package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
- package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
- package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
- package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
- package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
- package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
- package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
- package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
- package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
- package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
- package/content/knowledge/web-app/web-app-architecture.md +116 -0
- package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
- package/content/knowledge/web-app/web-app-conventions.md +121 -0
- package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
- package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
- package/content/knowledge/web-app/web-app-deployment.md +134 -0
- package/content/knowledge/web-app/web-app-design-system.md +158 -0
- package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
- package/content/knowledge/web-app/web-app-observability.md +221 -0
- package/content/knowledge/web-app/web-app-project-structure.md +160 -0
- package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
- package/content/knowledge/web-app/web-app-requirements.md +112 -0
- package/content/knowledge/web-app/web-app-security.md +193 -0
- package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
- package/content/knowledge/web-app/web-app-testing.md +249 -0
- package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
- package/content/methodology/backend-overlay.yml +73 -0
- package/content/methodology/cli-overlay.yml +69 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/content/methodology/web-app-overlay.yml +79 -0
- package/dist/cli/commands/init.d.ts +21 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +261 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +206 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +1392 -64
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +82 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +302 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +2 -1
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +56 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +16 -0
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +834 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -0
- package/dist/types/config.d.ts +19 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wizard/questions.d.ts +27 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +142 -3
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +206 -8
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +21 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +27 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/wizard.d.ts +0 -14
- package/dist/types/wizard.d.ts.map +0 -1
- package/dist/types/wizard.js +0 -2
- package/dist/types/wizard.js.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-app-conventions
|
|
3
|
+
description: Platform naming conventions, accessibility patterns, navigation patterns, and code style for iOS and Android mobile apps
|
|
4
|
+
topics: [mobile-app, conventions, swift, kotlin, naming, accessibility, navigation]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Mobile platform conventions exist for consistency across the ecosystem, toolchain compatibility, and team readability. iOS and Android have distinct naming conventions, navigation paradigms, and accessibility implementation patterns. Mixing conventions or importing web/backend naming styles into mobile code creates cognitive friction and breaks tooling assumptions (SwiftUI previews, Android Studio refactoring, lint rules).
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
iOS conventions use PascalCase for types and camelCase for everything else; Swift files are one type per file named identically to the type. Android uses PascalCase for classes and camelCase for members, with XML resource names in snake_case. Navigation uses UINavigationController/Coordinator (iOS) or Navigation Component with NavGraph (Android). Accessibility labels follow platform semantics: VoiceOver on iOS, TalkBack on Android. Follow these patterns consistently — tooling and platform reviewers expect them.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### iOS Naming Conventions (Swift)
|
|
16
|
+
|
|
17
|
+
**Types (classes, structs, enums, protocols)**
|
|
18
|
+
- PascalCase: `UserProfileViewController`, `AuthenticationService`, `PaymentStatus`
|
|
19
|
+
- Protocols: prefer noun or adjective+able naming. `Codable`, `Equatable`, `DataSource`, `Delegate`
|
|
20
|
+
- Protocol conformances add a semantic suffix: `UITableViewDataSource`, `URLSessionDelegate`
|
|
21
|
+
- Generic type parameters: single uppercase letter for simple cases (`T`, `U`), descriptive names for constrained types (`Element`, `Key`, `Value`)
|
|
22
|
+
|
|
23
|
+
**Properties, methods, and variables**
|
|
24
|
+
- camelCase: `firstName`, `isAuthenticated`, `fetchUserProfile()`
|
|
25
|
+
- Boolean properties: use `is`, `has`, `should`, `can` prefix: `isLoading`, `hasUnreadMessages`, `canSubmit`
|
|
26
|
+
- Avoid abbreviations except widely established ones: `url`, `id`, `api` are acceptable; `usrNm` is not
|
|
27
|
+
|
|
28
|
+
**File naming**
|
|
29
|
+
- One primary type per file, filename matches type exactly: `UserProfileViewController.swift`, `AuthenticationService.swift`
|
|
30
|
+
- Extensions can live in separate files: `User+Codable.swift`, `View+Accessibility.swift`
|
|
31
|
+
- Protocol conformances in extension files when they're substantial: `UserProfileViewController+TableViewDelegate.swift`
|
|
32
|
+
|
|
33
|
+
**View naming (UIKit)**
|
|
34
|
+
- ViewControllers always end in `ViewController`: `LoginViewController`, `SettingsViewController`
|
|
35
|
+
- Views end in `View`: `ProfileHeaderView`, `EmptyStateView`
|
|
36
|
+
- Cells end in `Cell`: `ProductListCell`, `MessageBubbleCell`
|
|
37
|
+
- Reuse identifiers match class names: `tableView.register(ProductListCell.self, forCellReuseIdentifier: "ProductListCell")`
|
|
38
|
+
|
|
39
|
+
**SwiftUI naming**
|
|
40
|
+
- Views are structs with descriptive names: `UserProfileView`, `LoginForm`, `SettingsRow`
|
|
41
|
+
- ViewModels are `@Observable` classes or ObservableObjects ending in `ViewModel`: `UserProfileViewModel`
|
|
42
|
+
- Preview providers: `#Preview { UserProfileView() }`
|
|
43
|
+
|
|
44
|
+
**Constants**
|
|
45
|
+
- Use `enum` as a namespace (no-case enums cannot be instantiated): `enum APIConstants { static let baseURL = "..." }`
|
|
46
|
+
- `static let` on structs/classes for type-level constants
|
|
47
|
+
- Avoid global `let` at file scope except for truly global constants
|
|
48
|
+
|
|
49
|
+
### Android Naming Conventions (Kotlin)
|
|
50
|
+
|
|
51
|
+
**Classes and interfaces**
|
|
52
|
+
- PascalCase: `UserProfileFragment`, `AuthRepository`, `PaymentUseCase`
|
|
53
|
+
- Activities: end in `Activity` — `MainActivity`, `LoginActivity`
|
|
54
|
+
- Fragments: end in `Fragment` — `UserProfileFragment`, `SettingsFragment`
|
|
55
|
+
- ViewModels: end in `ViewModel` — `UserProfileViewModel`, `HomeViewModel`
|
|
56
|
+
- Repositories: end in `Repository` — `UserRepository`, `ProductRepository`
|
|
57
|
+
- Use cases: verb phrase — `GetUserProfileUseCase`, `UpdateSettingsUseCase`
|
|
58
|
+
|
|
59
|
+
**Properties and functions**
|
|
60
|
+
- camelCase: `firstName`, `isAuthenticated`, `fetchUserProfile()`
|
|
61
|
+
- Boolean properties: `is`, `has`, `should` prefix — `isLoading`, `hasError`, `shouldShowEmpty`
|
|
62
|
+
- Extension functions follow the same rules: fun `String.isValidEmail()`, fun `View.setVisible(Boolean)`
|
|
63
|
+
|
|
64
|
+
**XML resource naming (snake_case always)**
|
|
65
|
+
- Layouts: `{type}_{name}.xml` — `activity_main.xml`, `fragment_profile.xml`, `item_product.xml`, `view_empty_state.xml`
|
|
66
|
+
- IDs: `{type}_{name}` — `@id/button_submit`, `@id/text_username`, `@id/recycler_products`
|
|
67
|
+
- Drawables: `ic_{name}.xml` for icons, `bg_{name}.xml` for backgrounds, `shape_{name}.xml` for shapes
|
|
68
|
+
- Colors: descriptive names in `colors.xml` (`color_primary`, `color_surface`), semantic names in theme (`colorPrimary`, `colorSurface`)
|
|
69
|
+
- Strings: `{screen}_{element}_{type}` — `login_email_hint`, `profile_name_label`, `error_network_message`
|
|
70
|
+
- Dimensions: `{component}_{property}` — `button_corner_radius`, `card_elevation`, `spacing_large`
|
|
71
|
+
|
|
72
|
+
**Compose naming**
|
|
73
|
+
- Composables: PascalCase like types: `UserProfileScreen`, `ProductCard`, `EmptyState`
|
|
74
|
+
- State holders ending in `State`: `LoginUiState`, `ProfileUiState`
|
|
75
|
+
- Preview functions: `@Preview @Composable fun UserProfileScreenPreview()`
|
|
76
|
+
|
|
77
|
+
### Navigation Patterns — iOS
|
|
78
|
+
|
|
79
|
+
**UIKit navigation patterns**
|
|
80
|
+
|
|
81
|
+
*UINavigationController (hierarchical)*
|
|
82
|
+
- Push/pop model for drill-down navigation: settings list → individual setting
|
|
83
|
+
- `navigationController?.pushViewController(vc, animated: true)` / `.popViewController(animated: true)`
|
|
84
|
+
- Always use `navigationItem.title` and `navigationItem.backButtonTitle` for VoiceOver accessibility
|
|
85
|
+
|
|
86
|
+
*UITabBarController (top-level)*
|
|
87
|
+
- Manage 2–5 coordinate sibling views; do not nest tab bars
|
|
88
|
+
- Each tab owns its own `UINavigationController` — this is the standard pattern
|
|
89
|
+
- Tab bar items need `title` and `image` (SF Symbol): `UITabBarItem(title: "Home", image: UIImage(systemName: "house"), tag: 0)`
|
|
90
|
+
|
|
91
|
+
*Modal presentations*
|
|
92
|
+
- `.present(_:animated:completion:)` for modals
|
|
93
|
+
- Always provide a dismissal mechanism (button or swipe-down) — never trap the user in a modal
|
|
94
|
+
- `UISheetPresentationController` for bottom sheets (iOS 15+): `detents: [.medium(), .large()]`
|
|
95
|
+
|
|
96
|
+
*Coordinator pattern*
|
|
97
|
+
- Extract navigation logic from ViewControllers into Coordinator objects
|
|
98
|
+
- Each Coordinator owns a `UINavigationController` and creates/presents child ViewControllers
|
|
99
|
+
- Child Coordinators are stored in a `childCoordinators` array — release them in the finish callback to prevent leaks
|
|
100
|
+
- Delegate back to parent Coordinator for cross-boundary navigation
|
|
101
|
+
|
|
102
|
+
**SwiftUI navigation**
|
|
103
|
+
- `NavigationStack` (iOS 16+) for hierarchical navigation: `NavigationStack(path: $path) { ... }`
|
|
104
|
+
- `NavigationPath` for type-erased programmatic navigation
|
|
105
|
+
- `.navigationDestination(for:)` to register destination views for path types
|
|
106
|
+
- `TabView` with `tabItem` modifier for tab navigation
|
|
107
|
+
- `.sheet`, `.fullScreenCover`, `.popover` for modal presentation
|
|
108
|
+
- Pass navigation state via bindings or a `NavigationRouter` observable object — do not use environment objects for navigation state in deep hierarchies
|
|
109
|
+
|
|
110
|
+
### Navigation Patterns — Android
|
|
111
|
+
|
|
112
|
+
**Navigation Component (Jetpack)**
|
|
113
|
+
- Define navigation graph in `nav_graph.xml` or `NavHost` composable
|
|
114
|
+
- Fragments connect via Actions defined in the nav graph
|
|
115
|
+
- Pass arguments with Safe Args plugin — type-safe navigation prevents bundle key typos
|
|
116
|
+
- Deep links registered in nav graph enable external app navigation
|
|
117
|
+
|
|
118
|
+
**Compose Navigation**
|
|
119
|
+
- `NavHost` with `composable` routes: `NavHost(navController, startDestination = "home") { composable("home") { HomeScreen(navController) } }`
|
|
120
|
+
- Route strings are stringly-typed — define as constants in a `Screen` sealed class or object
|
|
121
|
+
- Pass data via route arguments (`/{userId}`) or ViewModel shared across the back stack
|
|
122
|
+
- `rememberNavController()` at the top of the composition; pass `NavController` down as a lambda (`onNavigate: () -> Unit`) rather than passing the controller itself
|
|
123
|
+
|
|
124
|
+
**Back stack management**
|
|
125
|
+
- `popBackStack()` for simple back navigation
|
|
126
|
+
- `navigate("destination") { popUpTo("home") { inclusive = false } }` to clear the back stack when navigating to a top-level destination
|
|
127
|
+
- `launchSingleTop = true` to prevent duplicate destinations in the back stack
|
|
128
|
+
|
|
129
|
+
### Accessibility Conventions
|
|
130
|
+
|
|
131
|
+
**iOS VoiceOver implementation**
|
|
132
|
+
|
|
133
|
+
Labels vs. hints:
|
|
134
|
+
- `accessibilityLabel`: what the element is — "Profile photo for Jane Smith"
|
|
135
|
+
- `accessibilityHint`: what happens when activated — "Double-tap to view full profile"
|
|
136
|
+
- Never include the element type in the label (VoiceOver announces it separately): say "Submit" not "Submit button"
|
|
137
|
+
|
|
138
|
+
Grouping:
|
|
139
|
+
- Use `accessibilityElements` to define a custom element order within a container
|
|
140
|
+
- `UIAccessibilityElement` with `isAccessibilityElement = true` for custom drawing views
|
|
141
|
+
- `accessibilityActivate()` override for complex controls with custom activation behavior
|
|
142
|
+
|
|
143
|
+
SwiftUI accessibility:
|
|
144
|
+
- `.accessibilityLabel("Submit order")` overrides the computed label
|
|
145
|
+
- `.accessibilityHint("Processes payment and places order")` adds the activation hint
|
|
146
|
+
- `.accessibilityAddTraits(.isButton)` when a custom view should be announced as a button
|
|
147
|
+
- `.accessibilityRemoveTraits(.isImage)` when an image is decorative
|
|
148
|
+
- `.accessibilityElement(children: .combine)` to merge a container's children into one accessible element
|
|
149
|
+
|
|
150
|
+
**Android TalkBack implementation**
|
|
151
|
+
|
|
152
|
+
View system:
|
|
153
|
+
- `android:contentDescription="@string/submit_button_label"` on all non-text interactive views
|
|
154
|
+
- `android:importantForAccessibility="no"` for decorative views (icons that duplicate adjacent text)
|
|
155
|
+
- `android:labelFor="@id/edit_text_email"` on Label TextViews — associates the label with the input for TalkBack
|
|
156
|
+
- `ViewCompat.setAccessibilityDelegate()` for advanced semantic customization
|
|
157
|
+
|
|
158
|
+
Compose semantics:
|
|
159
|
+
- `Modifier.semantics { contentDescription = "Submit order" }` for custom content descriptions
|
|
160
|
+
- `Modifier.semantics { role = Role.Button }` for role announcement
|
|
161
|
+
- `Modifier.clearAndSetSemantics { ... }` to replace inherited semantics entirely
|
|
162
|
+
- `Modifier.semantics(mergeDescendants = true) { ... }` to merge a group of elements
|
|
163
|
+
|
|
164
|
+
### Code Style Conventions
|
|
165
|
+
|
|
166
|
+
**Swift**
|
|
167
|
+
- `guard` for early returns over nested `if` statements
|
|
168
|
+
- `let` by default; `var` only when mutation is necessary
|
|
169
|
+
- Trailing closures when the last parameter is a closure: `array.filter { $0.isActive }`
|
|
170
|
+
- `@discardableResult` on functions when the return value may intentionally be ignored
|
|
171
|
+
- Mark protocol methods with default implementations in extensions
|
|
172
|
+
- Avoid `!` force-unwrap; use `guard let` or `if let` with meaningful error handling
|
|
173
|
+
|
|
174
|
+
**Kotlin**
|
|
175
|
+
- `val` by default; `var` only when mutation is required
|
|
176
|
+
- Data classes for value types that need `equals`/`hashCode`/`copy`: `data class User(val id: String, val name: String)`
|
|
177
|
+
- Sealed classes for exhaustive state modeling: `sealed class UiState { data class Success(...); data object Loading; data class Error(...) }`
|
|
178
|
+
- Extension functions over utility classes: `fun String.isValidEmail()` not `EmailUtils.isValidEmail(string)`
|
|
179
|
+
- Coroutines over threads/callbacks; `suspend` functions for async operations
|
|
180
|
+
- `Flow` for reactive streams; `StateFlow` for UI state; `SharedFlow` for events
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-app-deployment
|
|
3
|
+
description: App store submission, code signing, provisioning profiles, CI/CD with Fastlane, and release management for iOS and Android
|
|
4
|
+
topics: [mobile-app, deployment, app-store, google-play, code-signing, fastlane, ci-cd, release-management]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Mobile app deployment is significantly more complex than web deployment: code signing creates a cryptographic chain of trust, app store review is a human process with 24–48 hour latency, and binary deployment means bugs cannot be hot-patched without a full submission cycle. Automate as much of this as possible with Fastlane — manual signing and upload processes are error-prone and do not scale to frequent releases.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
iOS deployment requires Apple Developer account, code signing (certificates + provisioning profiles), App Store Connect submission, and 24–48 hour review. Android deployment requires a Google Play Developer account, APK/AAB signing with a release keystore, and Play Store submission. Both platforms support CI/CD automation via Fastlane lanes. Code signing is the most failure-prone step — use Fastlane Match (iOS) or a secrets-managed keystore (Android) to make it reproducible. Automate the full pipeline from test → build → sign → upload.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### iOS Code Signing
|
|
16
|
+
|
|
17
|
+
**Concepts**
|
|
18
|
+
- **Certificate**: A key pair issued by Apple. Two types relevant to development: Apple Distribution (for App Store) and Apple Development (for device testing).
|
|
19
|
+
- **App ID**: A unique identifier (`com.example.myapp`) registered in the Apple Developer portal.
|
|
20
|
+
- **Provisioning Profile**: A file that binds an App ID to a certificate and, for development, to specific device UDIDs. Must be re-downloaded when devices are added.
|
|
21
|
+
- **Entitlements**: Capabilities your app uses (push notifications, in-app purchases, Sign in with Apple) — must match between app target and provisioning profile.
|
|
22
|
+
|
|
23
|
+
**Automatic vs. manual signing**
|
|
24
|
+
- Automatic (Xcode manages signing): fine for individual developers, unreliable in CI — Xcode modifies the project file.
|
|
25
|
+
- Manual signing: specify `CODE_SIGN_IDENTITY`, `PROVISIONING_PROFILE_SPECIFIER` explicitly in xcconfig or build settings. Required for reliable CI.
|
|
26
|
+
|
|
27
|
+
**Fastlane Match for team signing**
|
|
28
|
+
Match stores certificates and profiles in a git repository (or S3/Google Cloud), encrypted with a passphrase. Every team member and CI runner fetches from the same source:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Matchfile
|
|
32
|
+
git_url("https://github.com/example/certificates")
|
|
33
|
+
storage_mode("git")
|
|
34
|
+
type("appstore") # or "development", "adhoc"
|
|
35
|
+
app_identifier("com.example.myapp")
|
|
36
|
+
|
|
37
|
+
# Fastfile
|
|
38
|
+
lane :sync_signing do
|
|
39
|
+
match(type: "appstore", readonly: is_ci)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
lane :build_release do
|
|
43
|
+
sync_signing
|
|
44
|
+
gym(
|
|
45
|
+
scheme: "MyApp",
|
|
46
|
+
configuration: "Release",
|
|
47
|
+
export_method: "app-store",
|
|
48
|
+
output_directory: "./build"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Match setup workflow**
|
|
54
|
+
```bash
|
|
55
|
+
# First time: create certificates repo and generate certs
|
|
56
|
+
fastlane match init
|
|
57
|
+
fastlane match appstore # generates Distribution cert + App Store profile
|
|
58
|
+
fastlane match development # generates Development cert + profile
|
|
59
|
+
|
|
60
|
+
# Subsequent: sync to CI or new dev machine
|
|
61
|
+
MATCH_PASSWORD=<passphrase> fastlane match appstore --readonly
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**CI signing setup**
|
|
65
|
+
- Store `MATCH_PASSWORD` as a CI secret variable — never commit it
|
|
66
|
+
- Use `readonly: true` in CI (`is_ci` returns true in most CI environments) — CI should never regenerate certificates
|
|
67
|
+
- For GitHub Actions: store the certificates git repo URL and match password as repository secrets
|
|
68
|
+
|
|
69
|
+
### iOS App Store Submission
|
|
70
|
+
|
|
71
|
+
**App Store Connect setup**
|
|
72
|
+
1. Create the app record in App Store Connect (appstoreconnect.apple.com)
|
|
73
|
+
2. Configure capabilities in the Apple Developer portal (push notifications, Sign in with Apple, etc.)
|
|
74
|
+
3. Create App Store listing: screenshots (required for each device size), description, keywords, privacy policy URL
|
|
75
|
+
4. Configure pricing and availability
|
|
76
|
+
|
|
77
|
+
**Required screenshots (2024)**
|
|
78
|
+
- iPhone: 6.9" display (iPhone 16 Pro Max), 6.5" display (iPhone 14 Plus)
|
|
79
|
+
- iPad (if universal): 12.9" iPad Pro, 11" iPad Pro
|
|
80
|
+
- Screenshots generated programmatically with `fastlane snapshot` + `fastlane frameit`
|
|
81
|
+
|
|
82
|
+
**Fastlane deliver for metadata + upload**
|
|
83
|
+
```ruby
|
|
84
|
+
lane :release do
|
|
85
|
+
# Build
|
|
86
|
+
gym(scheme: "MyApp", configuration: "Release")
|
|
87
|
+
|
|
88
|
+
# Upload to App Store Connect
|
|
89
|
+
deliver(
|
|
90
|
+
submit_for_review: false, # set true to auto-submit
|
|
91
|
+
automatic_release: false, # set true to auto-release after approval
|
|
92
|
+
force: true, # skip HTML report generation
|
|
93
|
+
metadata_path: "./fastlane/metadata",
|
|
94
|
+
screenshots_path: "./fastlane/screenshots"
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**App Store review guidelines (commonly rejected items)**
|
|
100
|
+
- Crashy builds: any crash during review results in immediate rejection
|
|
101
|
+
- Incomplete functionality: demo/placeholder screens visible to reviewers
|
|
102
|
+
- Login-gated apps: must provide reviewer credentials in App Store Connect
|
|
103
|
+
- Guideline 4.2.2: apps must be more than a repackaged website
|
|
104
|
+
- Push notification permission: must explain usage before prompting
|
|
105
|
+
- Privacy labels must accurately describe all data collected
|
|
106
|
+
|
|
107
|
+
**TestFlight for pre-release distribution**
|
|
108
|
+
```ruby
|
|
109
|
+
lane :beta do
|
|
110
|
+
gym(scheme: "MyApp", configuration: "Release")
|
|
111
|
+
pilot(
|
|
112
|
+
app_identifier: "com.example.myapp",
|
|
113
|
+
changelog: "Bug fixes and improvements",
|
|
114
|
+
distribute_external: false, # true for external testers
|
|
115
|
+
notify_external_testers: false
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Android Code Signing
|
|
121
|
+
|
|
122
|
+
**Release keystore**
|
|
123
|
+
```bash
|
|
124
|
+
# Generate a release keystore (do this once; store securely)
|
|
125
|
+
keytool -genkey -v \
|
|
126
|
+
-keystore release.keystore \
|
|
127
|
+
-alias myapp \
|
|
128
|
+
-keyalg RSA \
|
|
129
|
+
-keysize 2048 \
|
|
130
|
+
-validity 10000
|
|
131
|
+
|
|
132
|
+
# Never commit release.keystore to git
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Signing configuration in Gradle**
|
|
136
|
+
```kotlin
|
|
137
|
+
// app/build.gradle.kts
|
|
138
|
+
android {
|
|
139
|
+
signingConfigs {
|
|
140
|
+
create("release") {
|
|
141
|
+
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "release.keystore")
|
|
142
|
+
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
|
143
|
+
keyAlias = System.getenv("KEY_ALIAS")
|
|
144
|
+
keyPassword = System.getenv("KEY_PASSWORD")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
buildTypes {
|
|
148
|
+
release {
|
|
149
|
+
signingConfig = signingConfigs.getByName("release")
|
|
150
|
+
isMinifyEnabled = true
|
|
151
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**CI keystore management**
|
|
158
|
+
- Store keystore as a base64-encoded CI secret: `base64 release.keystore | pbcopy`
|
|
159
|
+
- In CI: decode and write to disk: `echo $KEYSTORE_BASE64 | base64 -d > release.keystore`
|
|
160
|
+
- Store `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` as separate CI secrets
|
|
161
|
+
- Never print these values in CI logs — mask secrets in CI configuration
|
|
162
|
+
|
|
163
|
+
**Google Play App Signing**
|
|
164
|
+
Enable Google Play App Signing: Google manages the release signing key, you upload with an upload key. Benefits: Google can re-sign if your upload key is lost; protects against keystore loss (catastrophic for Android apps — if you lose the keystore, you cannot update the app).
|
|
165
|
+
|
|
166
|
+
### Android Play Store Submission
|
|
167
|
+
|
|
168
|
+
**App Bundle (AAB) vs APK**
|
|
169
|
+
- Always submit AAB (`.aab`) to Play Store — it enables Play Feature Delivery, dynamic delivery, and smaller installs
|
|
170
|
+
- APK is for direct distribution only (enterprise, sideloading)
|
|
171
|
+
- Build AAB: `./gradlew bundleRelease`
|
|
172
|
+
|
|
173
|
+
**Fastlane supply for Android**
|
|
174
|
+
```ruby
|
|
175
|
+
lane :deploy_production do
|
|
176
|
+
gradle(task: "bundle", build_type: "Release")
|
|
177
|
+
supply(
|
|
178
|
+
track: "production",
|
|
179
|
+
aab: "app/build/outputs/bundle/release/app-release.aab",
|
|
180
|
+
package_name: "com.example.myapp"
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
lane :deploy_internal do
|
|
185
|
+
gradle(task: "bundle", build_type: "Release")
|
|
186
|
+
supply(
|
|
187
|
+
track: "internal",
|
|
188
|
+
aab: "app/build/outputs/bundle/release/app-release.aab"
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Play Console tracks**
|
|
194
|
+
- Internal testing: up to 100 testers, immediate availability
|
|
195
|
+
- Closed testing (Alpha): specific Google Groups, same-day availability
|
|
196
|
+
- Open testing (Beta): public opt-in, same-day availability
|
|
197
|
+
- Production: staged rollout available (1% → 5% → 20% → 100%)
|
|
198
|
+
|
|
199
|
+
**ProGuard / R8 rules**
|
|
200
|
+
```proguard
|
|
201
|
+
# Keep data classes used by Gson/Moshi/Retrofit
|
|
202
|
+
-keep class com.example.myapp.data.model.** { *; }
|
|
203
|
+
|
|
204
|
+
# Keep Retrofit service interfaces
|
|
205
|
+
-keep interface com.example.myapp.data.network.** { *; }
|
|
206
|
+
|
|
207
|
+
# Keep Parcelable implementations
|
|
208
|
+
-keep class * implements android.os.Parcelable {
|
|
209
|
+
public static final android.os.Parcelable$Creator *;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Always build and test the release APK/AAB locally before submitting — R8 obfuscation can break reflection-dependent code (Gson, Retrofit, Hilt) that works fine in debug builds.
|
|
214
|
+
|
|
215
|
+
### CI/CD Pipeline
|
|
216
|
+
|
|
217
|
+
**GitHub Actions — iOS**
|
|
218
|
+
```yaml
|
|
219
|
+
name: iOS Release
|
|
220
|
+
on:
|
|
221
|
+
push:
|
|
222
|
+
tags: ['v*']
|
|
223
|
+
jobs:
|
|
224
|
+
release:
|
|
225
|
+
runs-on: macos-14
|
|
226
|
+
steps:
|
|
227
|
+
- uses: actions/checkout@v4
|
|
228
|
+
- uses: ruby/setup-ruby@v1
|
|
229
|
+
with:
|
|
230
|
+
bundler-cache: true
|
|
231
|
+
- name: Install dependencies
|
|
232
|
+
run: bundle exec pod install
|
|
233
|
+
- name: Sync signing
|
|
234
|
+
env:
|
|
235
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
236
|
+
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
|
|
237
|
+
run: bundle exec fastlane sync_signing
|
|
238
|
+
- name: Build and upload
|
|
239
|
+
env:
|
|
240
|
+
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
241
|
+
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
242
|
+
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
|
|
243
|
+
run: bundle exec fastlane release
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**GitHub Actions — Android**
|
|
247
|
+
```yaml
|
|
248
|
+
name: Android Release
|
|
249
|
+
on:
|
|
250
|
+
push:
|
|
251
|
+
tags: ['v*']
|
|
252
|
+
jobs:
|
|
253
|
+
release:
|
|
254
|
+
runs-on: ubuntu-latest
|
|
255
|
+
steps:
|
|
256
|
+
- uses: actions/checkout@v4
|
|
257
|
+
- uses: actions/setup-java@v4
|
|
258
|
+
with:
|
|
259
|
+
java-version: '17'
|
|
260
|
+
distribution: 'temurin'
|
|
261
|
+
- name: Decode keystore
|
|
262
|
+
env:
|
|
263
|
+
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
|
264
|
+
run: echo $KEYSTORE_BASE64 | base64 -d > release.keystore
|
|
265
|
+
- name: Build release
|
|
266
|
+
env:
|
|
267
|
+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
|
268
|
+
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
269
|
+
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
270
|
+
run: ./gradlew bundleRelease
|
|
271
|
+
- name: Upload to Play Store
|
|
272
|
+
uses: r0adkll/upload-google-play@v1
|
|
273
|
+
with:
|
|
274
|
+
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
|
275
|
+
packageName: com.example.myapp
|
|
276
|
+
releaseFiles: app/build/outputs/bundle/release/*.aab
|
|
277
|
+
track: internal
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Version Management
|
|
281
|
+
|
|
282
|
+
**iOS versioning**
|
|
283
|
+
- `CFBundleShortVersionString` (Marketing version): user-visible version (`1.2.3`)
|
|
284
|
+
- `CFBundleVersion` (Build number): must increase monotonically for each App Store submission
|
|
285
|
+
- Automate build number increment: `fastlane run increment_build_number`
|
|
286
|
+
- Automate version: `fastlane run increment_version_number bump_type:minor`
|
|
287
|
+
|
|
288
|
+
**Android versioning**
|
|
289
|
+
```kotlin
|
|
290
|
+
android {
|
|
291
|
+
defaultConfig {
|
|
292
|
+
versionCode = 42 // Must increase on every Play Store submission
|
|
293
|
+
versionName = "1.2.3" // User-visible version string
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Automate versionCode in CI: read from git tag, CI build number, or a `version.properties` file.
|