@moifort/stack 0.0.0-stage → 0.1.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.
Files changed (263) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -2
  3. package/bin/stack.ts +109 -0
  4. package/hooks/shutdown-simulators-after-push.sh +19 -0
  5. package/package.json +40 -4
  6. package/routines/daily-health.md +78 -0
  7. package/routines/deprecations.md +23 -0
  8. package/routines/sentry-autofix.md +76 -0
  9. package/routines/stack-upgrade.md +45 -0
  10. package/rules/00-collaboration.md +81 -0
  11. package/rules/01-workflow.md +81 -0
  12. package/rules/02-commits.md +71 -0
  13. package/rules/03-code-review.md +25 -0
  14. package/rules/04-push.md +75 -0
  15. package/rules/05-release.md +40 -0
  16. package/rules/06-changelog.md +48 -0
  17. package/rules/07-appstore.md +77 -0
  18. package/rules/08-copy.md +52 -0
  19. package/rules/09-backend.md +132 -0
  20. package/rules/10-ios.md +206 -0
  21. package/rules/11-ops.md +114 -0
  22. package/rules/12-ci.md +32 -0
  23. package/rules/13-ai.md +71 -0
  24. package/rules/14-infra.md +90 -0
  25. package/rules/15-tooling.md +81 -0
  26. package/rules/16-routines.md +69 -0
  27. package/rules/17-landing.md +76 -0
  28. package/skills/appstore/SKILL.md +53 -0
  29. package/skills/domain/SKILL.md +136 -0
  30. package/skills/graphql/SKILL.md +83 -0
  31. package/skills/ios-feature/SKILL.md +94 -0
  32. package/skills/stack-install/SKILL.md +35 -0
  33. package/skills/stack-upgrade/SKILL.md +27 -0
  34. package/src/catalogue.ts +33 -0
  35. package/src/config.ts +20 -0
  36. package/src/init.ts +63 -0
  37. package/src/migrations.ts +72 -0
  38. package/src/migrations.unit.test.ts +44 -0
  39. package/src/package.ts +8 -0
  40. package/src/render.ts +13 -0
  41. package/src/render.unit.test.ts +17 -0
  42. package/src/sync.int.test.ts +103 -0
  43. package/src/sync.ts +161 -0
  44. package/src/templates.ts +121 -0
  45. package/src/validate-content.ts +65 -0
  46. package/templates/backend/.env.example +10 -0
  47. package/templates/backend/.github/workflows/deploy.yml +151 -0
  48. package/templates/backend/.github/workflows/schema.yml +59 -0
  49. package/templates/backend/.github/workflows/test-feat.yml +40 -0
  50. package/templates/backend/.github/workflows/test-int.yml +40 -0
  51. package/templates/backend/.github/workflows/test-unit.yml +64 -0
  52. package/templates/backend/docs/api-evolution.md +67 -0
  53. package/templates/backend/firebase.json +21 -0
  54. package/templates/backend/firestore.indexes.json +4 -0
  55. package/templates/backend/firestore.rules +11 -0
  56. package/templates/backend/infra/auth.tf +54 -0
  57. package/templates/backend/infra/backend.tf +6 -0
  58. package/templates/backend/infra/cicd.tf +63 -0
  59. package/templates/backend/infra/firebase.tf +26 -0
  60. package/templates/backend/infra/firestore.tf +53 -0
  61. package/templates/backend/infra/function.tf +171 -0
  62. package/templates/backend/infra/main.tf +26 -0
  63. package/templates/backend/infra/observer.tf +18 -0
  64. package/templates/backend/infra/outputs.tf +30 -0
  65. package/templates/backend/infra/project.tf +64 -0
  66. package/templates/backend/infra/scheduler.tf +28 -0
  67. package/templates/backend/infra/secrets.tf +44 -0
  68. package/templates/backend/infra/state.tf +13 -0
  69. package/templates/backend/infra/terraform.tfvars.example +16 -0
  70. package/templates/backend/infra/variables.tf +104 -0
  71. package/templates/backend/infra/versions.tf +30 -0
  72. package/templates/backend/nitro.config.ts +40 -0
  73. package/templates/backend/package.json +51 -0
  74. package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
  75. package/templates/backend/scripts/deprecations/status.ts +50 -0
  76. package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
  77. package/templates/backend/scripts/deprecations/usage.ts +79 -0
  78. package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
  79. package/templates/backend/scripts/deprecations.ts +224 -0
  80. package/templates/backend/scripts/export-graphql-schema.ts +11 -0
  81. package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
  82. package/templates/backend/scripts/install-terraform.sh +61 -0
  83. package/templates/backend/scripts/tf +11 -0
  84. package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
  85. package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
  86. package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
  87. package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
  88. package/templates/backend/server/domain/changelog/primitives.ts +19 -0
  89. package/templates/backend/server/domain/changelog/query.ts +13 -0
  90. package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
  91. package/templates/backend/server/domain/changelog/types.ts +9 -0
  92. package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
  93. package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
  94. package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
  95. package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
  96. package/templates/backend/server/domain/shared/language.ts +26 -0
  97. package/templates/backend/server/domain/shared/page.ts +25 -0
  98. package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
  99. package/templates/backend/server/domain/shared/primitives.ts +50 -0
  100. package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
  101. package/templates/backend/server/domain/shared/types.ts +14 -0
  102. package/templates/backend/server/domain/user/command.ts +16 -0
  103. package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
  104. package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
  105. package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
  106. package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
  107. package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
  108. package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
  109. package/templates/backend/server/domain/user/query.int.test.ts +49 -0
  110. package/templates/backend/server/domain/user/query.ts +34 -0
  111. package/templates/backend/server/domain/user/types.ts +20 -0
  112. package/templates/backend/server/domain/user/use-case.ts +18 -0
  113. package/templates/backend/server/middleware/auth.ts +54 -0
  114. package/templates/backend/server/plugins/00-sentry.ts +74 -0
  115. package/templates/backend/server/plugins/01-request-log.ts +28 -0
  116. package/templates/backend/server/plugins/02-graphql.ts +69 -0
  117. package/templates/backend/server/plugins/03-boot-time.ts +21 -0
  118. package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
  119. package/templates/backend/server/routes/app-config.get.ts +15 -0
  120. package/templates/backend/server/routes/graphql.ts +52 -0
  121. package/templates/backend/server/system/app-support.ts +17 -0
  122. package/templates/backend/server/system/config/index.ts +18 -0
  123. package/templates/backend/server/system/config/primitives.ts +20 -0
  124. package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
  125. package/templates/backend/server/system/config/types.ts +8 -0
  126. package/templates/backend/server/system/firebase.ts +19 -0
  127. package/templates/backend/server/system/identity.ts +18 -0
  128. package/templates/backend/server/system/logger.ts +119 -0
  129. package/templates/backend/server/system/logger.unit.test.ts +109 -0
  130. package/templates/backend/server/system/migration/migrations/index.ts +6 -0
  131. package/templates/backend/server/system/migration/primitives.ts +18 -0
  132. package/templates/backend/server/system/migration/runner.ts +52 -0
  133. package/templates/backend/server/system/migration/types.ts +16 -0
  134. package/templates/backend/server/system/read-meter.ts +100 -0
  135. package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
  136. package/templates/backend/server/system/request-cache.ts +139 -0
  137. package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
  138. package/templates/backend/server/system/secret-box.ts +49 -0
  139. package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
  140. package/templates/backend/server/test/bdd.ts +12 -0
  141. package/templates/backend/server/test/fake-firestore.ts +483 -0
  142. package/templates/backend/server/utils/apollo.ts +13 -0
  143. package/templates/backend/server/utils/fetch.ts +5 -0
  144. package/templates/backend/server/utils/firestore.ts +91 -0
  145. package/templates/backend/server/utils/input.ts +36 -0
  146. package/templates/backend/server/utils/input.unit.test.ts +43 -0
  147. package/templates/backend/server/utils/slug.ts +21 -0
  148. package/templates/backend/tsconfig.json +16 -0
  149. package/templates/base/.github/workflows/renovate.yml +17 -0
  150. package/templates/base/CHANGELOG.fr.md +3 -0
  151. package/templates/base/CHANGELOG.md +7 -0
  152. package/templates/base/CLAUDE.md +34 -0
  153. package/templates/base/README.md +12 -0
  154. package/templates/base/biome.json +47 -0
  155. package/templates/base/bunfig.toml +2 -0
  156. package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
  157. package/templates/base/docs/data-recovery.md +116 -0
  158. package/templates/base/docs/ideas.md +4 -0
  159. package/templates/base/project-context.md +43 -0
  160. package/templates/base/renovate.json +37 -0
  161. package/templates/ios/.github/workflows/build-ios.yml +168 -0
  162. package/templates/ios/.github/workflows/ios-upload.yml +308 -0
  163. package/templates/ios/.github/workflows/release-ios.yml +178 -0
  164. package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
  165. package/templates/ios/infra/Gemfile +5 -0
  166. package/templates/ios/infra/fastlane/Appfile +1 -0
  167. package/templates/ios/infra/fastlane/Fastfile +54 -0
  168. package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
  169. package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
  170. package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
  171. package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
  172. package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
  173. package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
  174. package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
  175. package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
  176. package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
  177. package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
  178. package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
  179. package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
  180. package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
  181. package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
  182. package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
  183. package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
  184. package/templates/ios/infra/ios.tf +28 -0
  185. package/templates/ios/ios/ExportOptions.plist +26 -0
  186. package/templates/ios/ios/apollo-codegen-config.json +16 -0
  187. package/templates/ios/ios/project.yml +94 -0
  188. package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
  189. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  190. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  191. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
  192. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  193. package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
  194. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
  195. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
  196. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
  197. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
  198. package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
  199. package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
  200. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
  201. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
  202. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
  203. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
  204. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
  205. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
  206. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
  207. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
  208. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
  209. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
  210. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
  211. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
  212. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
  213. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
  214. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
  215. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
  216. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
  217. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
  218. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
  219. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
  220. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
  221. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
  222. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
  223. package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
  224. package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
  225. package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
  226. package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
  227. package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
  228. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
  229. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
  230. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
  231. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
  232. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
  233. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
  234. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
  235. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
  236. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
  237. package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
  238. package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
  239. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
  240. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
  241. package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
  242. package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
  243. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
  244. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
  245. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
  246. package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
  247. package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
  248. package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
  249. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
  250. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
  251. package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
  252. package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
  253. package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
  254. package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
  255. package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
  256. package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
  257. package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
  258. package/templates/ios/package.merge.json +10 -0
  259. package/templates/ios/scripts/apollo-codegen.ts +31 -0
  260. package/templates/ios/scripts/check-ios-components.ts +72 -0
  261. package/templates/ios/scripts/check-strings.ts +103 -0
  262. package/templates/ios/scripts/install-device.sh +29 -0
  263. package/templates/ios/scripts/testflight.ts +213 -0
@@ -0,0 +1,308 @@
1
+ name: iOS upload
2
+
3
+ # Archives, signs and uploads one build to App Store Connect. Called by release-ios.yml (a
4
+ # tagged release, then submitted for review) and by testflight-ios.yml (the nightly build for
5
+ # the internal TestFlight group): the signing is the same, only the trigger and what happens
6
+ # to the build afterwards differ.
7
+
8
+ on:
9
+ workflow_call:
10
+ inputs:
11
+ marketing_version:
12
+ description: 'Marketing version (CFBundleShortVersionString), e.g. 1.1'
13
+ type: string
14
+ required: true
15
+ skip_upload:
16
+ description: 'Archive and push the symbols to Sentry, but stop before App Store Connect'
17
+ type: boolean
18
+ default: false
19
+ outputs:
20
+ build:
21
+ description: 'The build number (CFBundleVersion): the commit count of HEAD'
22
+ value: ${{ jobs.check.outputs.build }}
23
+ exists:
24
+ description: 'Whether App Store Connect already had this build before the run'
25
+ value: ${{ jobs.check.outputs.exists }}
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ # The build number is the commit count, so it names a commit: a build that already exists
32
+ # on App Store Connect for this version is this very commit, uploaded before. Re-uploading
33
+ # it would be refused, and archiving it would spend forty minutes of macOS for nothing — so
34
+ # this is also what makes the nightly build a no-op when main has not moved.
35
+ check:
36
+ name: 🔎 Already on App Store Connect?
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 5
39
+ outputs:
40
+ build: ${{ steps.build.outputs.build }}
41
+ exists: ${{ steps.check.outputs.exists }}
42
+ previous: ${{ steps.check.outputs.previous }}
43
+ steps:
44
+ - uses: actions/checkout@v7
45
+ with:
46
+ fetch-depth: 0 # git rev-list --count HEAD must see the full history
47
+
48
+ - uses: oven-sh/setup-bun@v2
49
+
50
+ - name: Compute the build number
51
+ id: build
52
+ run: echo "build=$(git rev-list --count HEAD)" >> "$GITHUB_OUTPUT"
53
+
54
+ - name: Look the build up
55
+ id: check
56
+ env:
57
+ ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
58
+ ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
59
+ ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
60
+ MARKETING: ${{ inputs.marketing_version }}
61
+ BUILD: ${{ steps.build.outputs.build }}
62
+ run: |
63
+ set -euo pipefail
64
+ bun scripts/testflight.ts check "$MARKETING" "$BUILD" | tee -a "$GITHUB_OUTPUT"
65
+ if grep -q '^exists=true' "$GITHUB_OUTPUT"; then
66
+ echo "::notice::{{APP_NAME}} $MARKETING ($BUILD) is already on App Store Connect — nothing to upload"
67
+ fi
68
+
69
+ # With the `e2e` stack, an end-to-end job gates this one: `needs: [check, e2e]`.
70
+ upload:
71
+ name: 📱 iOS build & upload
72
+ needs: check
73
+ if: needs.check.outputs.exists != 'true'
74
+ runs-on: macos-26
75
+ timeout-minutes: 40
76
+ outputs:
77
+ uploaded: ${{ steps.upload.outputs.uploaded }}
78
+ env:
79
+ ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
80
+ ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
81
+ MARKETING: ${{ inputs.marketing_version }}
82
+ BUILD: ${{ needs.check.outputs.build }}
83
+ steps:
84
+ - uses: actions/checkout@v7
85
+ with:
86
+ fetch-depth: 0 # sentry-cli set-commits --local reads the history
87
+
88
+ - name: Select the latest final Xcode
89
+ uses: maxim-lobanov/setup-xcode@v1
90
+ with:
91
+ xcode-version: latest-stable
92
+
93
+ - name: Generate the Xcode project
94
+ run: |
95
+ brew install xcodegen
96
+ (cd ios && xcodegen generate)
97
+
98
+ - name: Materialize gitignored build files
99
+ env:
100
+ IOS_SENTRY_DSN: ${{ secrets.IOS_SENTRY_DSN }}
101
+ run: |
102
+ set -euo pipefail
103
+ printf '%s' "${{ secrets.IOS_GOOGLE_SERVICE_INFO_PLIST }}" \
104
+ | openssl base64 -d -A > ios/{{APP_NAME}}/GoogleService-Info.plist
105
+ # The archived build is the only one that reports to Sentry, and it reads the
106
+ # DSN from this gitignored file. Copying the example here would ship a build
107
+ # that reports nothing at all — no crash, no error, no user feedback — and
108
+ # nothing on the way would say so. Refuse to archive that build.
109
+ if [ -z "$IOS_SENTRY_DSN" ]; then
110
+ echo "::error::IOS_SENTRY_DSN is unset — this build could not report anything to Sentry"
111
+ exit 1
112
+ fi
113
+ cat > ios/{{APP_NAME}}/Shared/Secrets.swift <<SWIFT
114
+ import Foundation
115
+
116
+ enum Secrets {
117
+ static let apiToken = ""
118
+ static let sentryDsn = "$IOS_SENTRY_DSN"
119
+ }
120
+ SWIFT
121
+
122
+ - name: Install the App Store Connect API key
123
+ run: |
124
+ set -euo pipefail
125
+ mkdir -p "$HOME/.appstoreconnect/private_keys"
126
+ printf '%s' "${{ secrets.ASC_KEY_P8 }}" \
127
+ > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
128
+
129
+ - name: Import the distribution signing certificate
130
+ env:
131
+ IOS_DIST_CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }}
132
+ IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
133
+ run: |
134
+ set -euo pipefail
135
+ # A dedicated (non-managed) Apple Distribution cert + private key, so signing
136
+ # never depends on Xcode cloud-managed certificates (unusable from a fresh CI
137
+ # machine). The .p12 is base64-encoded in the secret; import it into a throwaway
138
+ # keychain that codesign/xcodebuild search during archive and export.
139
+ KEYCHAIN="$RUNNER_TEMP/{{APP_SLUG}}-signing.keychain-db"
140
+ KEYCHAIN_PASSWORD="$(uuidgen)"
141
+ CERT_PATH="$RUNNER_TEMP/dist-cert.p12"
142
+ printf '%s' "$IOS_DIST_CERT_P12" | base64 -d > "$CERT_PATH"
143
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
144
+ security set-keychain-settings -lut 21600 "$KEYCHAIN"
145
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
146
+ security import "$CERT_PATH" -P "$IOS_DIST_CERT_PASSWORD" \
147
+ -A -t cert -f pkcs12 -k "$KEYCHAIN"
148
+ security set-key-partition-list -S apple-tool:,apple: \
149
+ -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
150
+ # Prepend our keychain to the user search list so xcodebuild finds the identity.
151
+ security list-keychains -d user -s "$KEYCHAIN" \
152
+ $(security list-keychains -d user | sed 's/["[:space:]]//g')
153
+ rm -f "$CERT_PATH"
154
+ security find-identity -v -p codesigning "$KEYCHAIN"
155
+
156
+ - name: Generate Apollo GraphQL code (gitignored, regenerated per build)
157
+ run: |
158
+ set -euo pipefail
159
+ # Resolve SPM so the apollo-ios checkout (and its bundled CLI) exists.
160
+ xcodebuild -resolvePackageDependencies \
161
+ -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}}
162
+ # Apollo 2.x ships the CLI as a tarball inside the checkout — extract it.
163
+ CLI_DIR=$(find "$HOME/Library/Developer/Xcode/DerivedData" -type d \
164
+ -path "*/SourcePackages/checkouts/apollo-ios/CLI" | head -1)
165
+ tar -xzf "$CLI_DIR/apollo-ios-cli.tar.gz" -C "$CLI_DIR"
166
+ chmod +x "$CLI_DIR/apollo-ios-cli"
167
+ (cd ios && "$CLI_DIR/apollo-ios-cli" generate)
168
+
169
+ - name: Install the App Store provisioning profiles
170
+ env:
171
+ IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
172
+ run: |
173
+ set -euo pipefail
174
+ PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
175
+ mkdir -p "$PROFILES_DIR"
176
+ install_profile() {
177
+ local name="$1" encoded="$2"
178
+ if [ -z "$encoded" ]; then
179
+ echo "::error::$name is unset — create the profile in the Apple Developer portal and store it as a base64 secret"
180
+ exit 1
181
+ fi
182
+ # Xcode looks a profile up by the UUID in its file name. Reading it from
183
+ # the profile means a renewed profile only needs the secret replaced,
184
+ # not this step edited.
185
+ local path="$RUNNER_TEMP/$name.mobileprovision"
186
+ printf '%s' "$encoded" | base64 -d > "$path"
187
+ security cms -D -i "$path" > "$RUNNER_TEMP/$name.plist"
188
+ local uuid
189
+ uuid=$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$RUNNER_TEMP/$name.plist")
190
+ mv "$path" "$PROFILES_DIR/$uuid.mobileprovision"
191
+ echo "Installed provisioning profile $uuid ($name)"
192
+ }
193
+ install_profile IOS_PROVISION_PROFILE "$IOS_PROVISION_PROFILE"
194
+
195
+ - name: Archive (Release, manual signing with the installed cert and profile)
196
+ run: |
197
+ set -euo pipefail
198
+ # The Release configuration signs manually with the imported distribution
199
+ # certificate and the installed App Store profile, so the archive never talks
200
+ # to App Store Connect (its provisioning endpoint intermittently answers 401).
201
+ xcodebuild -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
202
+ -destination 'generic/platform=iOS' \
203
+ -archivePath build/{{APP_NAME}}.xcarchive \
204
+ MARKETING_VERSION="$MARKETING" \
205
+ CURRENT_PROJECT_VERSION="$BUILD" \
206
+ archive
207
+
208
+ - name: Upload the debug symbols to Sentry
209
+ env:
210
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
211
+ SENTRY_ORG: polyforms
212
+ SENTRY_PROJECT: {{APP_SLUG}}-ios
213
+ run: |
214
+ set -euo pipefail
215
+ # A crash report only names functions and lines if the dSYMs of that exact
216
+ # build reached Sentry. They live in the archive and nowhere else, so this
217
+ # has to happen here — an unsymbolicated crash cannot be fixed afterwards.
218
+ if [ -z "$SENTRY_AUTH_TOKEN" ]; then
219
+ echo "::warning::SENTRY_AUTH_TOKEN is unset — shipping a build Sentry cannot symbolicate"
220
+ exit 0
221
+ fi
222
+ curl -sL https://sentry.io/get-cli/ | bash
223
+ # --include-sources bundles the Swift sources alongside the symbols, so
224
+ # an issue shows the code around the crashing line instead of just its
225
+ # number.
226
+ sentry-cli debug-files upload --include-sources build/{{APP_NAME}}.xcarchive/dSYMs
227
+ # The Cocoa SDK stamps this exact release on every event it sends, so
228
+ # declaring it here with its commits is what lets an issue name the
229
+ # change that introduced it. --local reads the git history directly:
230
+ # no Sentry-GitHub integration to configure.
231
+ RELEASE="{{BUNDLE_ID}}@$MARKETING+$BUILD"
232
+ sentry-cli releases new "$RELEASE"
233
+ sentry-cli releases set-commits "$RELEASE" --local --ignore-missing
234
+ sentry-cli releases finalize "$RELEASE"
235
+
236
+ - name: Export a signed .ipa (manual signing)
237
+ run: |
238
+ set -euo pipefail
239
+ xcodebuild -exportArchive \
240
+ -archivePath build/{{APP_NAME}}.xcarchive \
241
+ -exportOptionsPlist ios/ExportOptions.plist \
242
+ -exportPath build/export
243
+
244
+ # Lets a manual run prove the archive and the Sentry symbol upload without
245
+ # spending a build number on App Store Connect. A tag push never sets this.
246
+ - name: Upload to App Store Connect
247
+ id: upload
248
+ if: ${{ inputs.skip_upload != true }}
249
+ run: |
250
+ set -euo pipefail
251
+ xcrun altool --upload-app --type ios \
252
+ --file build/export/{{APP_NAME}}.ipa \
253
+ --apiKey "$ASC_KEY_ID" \
254
+ --apiIssuer "$ASC_ISSUER_ID"
255
+ echo "uploaded=true" >> "$GITHUB_OUTPUT"
256
+
257
+ - name: Archive the .ipa as a run artifact
258
+ if: always()
259
+ uses: actions/upload-artifact@v7
260
+ with:
261
+ name: {{APP_NAME}}-ipa
262
+ path: |
263
+ build/export/*.ipa
264
+ if-no-files-found: ignore
265
+ retention-days: 14
266
+
267
+ # Every upload reaches the internal "Nightly" group on its own — the group has access to
268
+ # all builds and the app declares no non-exempt encryption, so there is no compliance
269
+ # question to answer. What is left is telling testers what changed: the commit subjects
270
+ # since the previous upload, whose build number is the commit count it was made from.
271
+ # The external "Public" group only sees the builds added to it, so each one is added and
272
+ # submitted to Beta App Review once processed.
273
+ notes:
274
+ name: 📝 TestFlight notes
275
+ needs: [check, upload]
276
+ if: needs.upload.outputs.uploaded == 'true'
277
+ runs-on: ubuntu-latest
278
+ timeout-minutes: 70
279
+ env:
280
+ ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
281
+ ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
282
+ ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
283
+ MARKETING: ${{ inputs.marketing_version }}
284
+ BUILD: ${{ needs.check.outputs.build }}
285
+ PREVIOUS: ${{ needs.check.outputs.previous }}
286
+ steps:
287
+ - uses: actions/checkout@v7
288
+ with:
289
+ fetch-depth: 0
290
+
291
+ - uses: oven-sh/setup-bun@v2
292
+
293
+ - name: Collect the commit subjects since the previous upload
294
+ run: |
295
+ set -euo pipefail
296
+ SINCE=""
297
+ if [[ "$PREVIOUS" =~ ^[0-9]+$ ]] && [ "$PREVIOUS" -lt "$BUILD" ]; then
298
+ SINCE=$(git rev-list --reverse HEAD | sed -n "${PREVIOUS}p")
299
+ fi
300
+ RANGE="${SINCE:+$SINCE..}HEAD"
301
+ git log --no-merges --max-count=30 --format='- %s' "$RANGE" > "$RUNNER_TEMP/whats-new.txt"
302
+ cat "$RUNNER_TEMP/whats-new.txt"
303
+
304
+ - name: Set What to Test and wait for processing
305
+ run: bun scripts/testflight.ts notes "$MARKETING" "$BUILD" "$RUNNER_TEMP/whats-new.txt"
306
+
307
+ - name: Share with the external Public group
308
+ run: bun scripts/testflight.ts share "$MARKETING" "$BUILD"
@@ -0,0 +1,178 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['ios-v*']
6
+ workflow_dispatch:
7
+ inputs:
8
+ marketing_version:
9
+ description: 'Marketing version (CFBundleShortVersionString), e.g. 1.1'
10
+ required: false
11
+ default: '1.0'
12
+ skip_upload:
13
+ description: 'Archive and push the symbols to Sentry, but stop before App Store Connect'
14
+ required: false
15
+ type: boolean
16
+ default: false
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ # Shared with testflight-ios.yml: two uploads at once would race for the same build number.
22
+ concurrency:
23
+ group: ios-upload
24
+ cancel-in-progress: false
25
+
26
+ jobs:
27
+ version:
28
+ name: 🏷️ Version
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 5
31
+ outputs:
32
+ marketing: ${{ steps.version.outputs.marketing }}
33
+ submitted: ${{ steps.submitted.outputs.submitted }}
34
+ steps:
35
+ - name: Take the marketing version from the tag, or from the manual run
36
+ id: version
37
+ env:
38
+ INPUT_VERSION: ${{ github.event.inputs.marketing_version }}
39
+ run: |
40
+ set -euo pipefail
41
+ if [ "$GITHUB_REF_TYPE" = "tag" ] && [ "${GITHUB_REF_NAME#ios-v}" != "$GITHUB_REF_NAME" ]; then
42
+ MARKETING="${GITHUB_REF_NAME#ios-v}"
43
+ else
44
+ MARKETING="$INPUT_VERSION"
45
+ fi
46
+ echo "marketing=$MARKETING" >> "$GITHUB_OUTPUT"
47
+ echo "Releasing {{APP_NAME}} $MARKETING"
48
+
49
+ - uses: actions/checkout@v7
50
+
51
+ - uses: oven-sh/setup-bun@v2
52
+
53
+ # A version sent to App Review by hand (the first subscription can only be attached
54
+ # from the web) is with Apple or on sale: nothing can be uploaded to it or
55
+ # submitted on it. Its tag then only records the release — the GitHub release is
56
+ # published, the upload and the submission are skipped.
57
+ - name: Is the version already with Apple?
58
+ id: submitted
59
+ env:
60
+ ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
61
+ ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
62
+ ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
63
+ MARKETING: ${{ steps.version.outputs.marketing }}
64
+ run: |
65
+ set -euo pipefail
66
+ bun scripts/testflight.ts submitted "$MARKETING" | tee -a "$GITHUB_OUTPUT"
67
+ if grep -q '^submitted=true' "$GITHUB_OUTPUT"; then
68
+ echo "::notice::{{APP_NAME}} $MARKETING is already with Apple — recording the tag, nothing uploaded or submitted"
69
+ fi
70
+
71
+ # Archive, sign and upload, shared with the nightly TestFlight build. When the tagged
72
+ # commit already went up as a nightly build of the same version, that binary is the one
73
+ # submitted: nothing is rebuilt.
74
+ release:
75
+ needs: version
76
+ if: needs.version.outputs.submitted != 'true'
77
+ uses: ./.github/workflows/ios-upload.yml
78
+ with:
79
+ marketing_version: ${{ needs.version.outputs.marketing }}
80
+ skip_upload: ${{ inputs.skip_upload == true }}
81
+ secrets: inherit
82
+
83
+ # After the binary is uploaded, wait for App Store Connect to finish processing it,
84
+ # then attach it to its version, set the "What's New" from CHANGELOG, and submit for
85
+ # review (auto-release on approval). No Xcode needed — runs on Linux. Tag pushes only;
86
+ # a manual workflow_dispatch stops after upload so it can be submitted by hand.
87
+ #
88
+ # The App Store panels are uploaded from the Mac before the tag, on request
89
+ # (stack rule 07-appstore.md): a version without them is refused by App Review.
90
+ submit:
91
+ name: 🍎 iOS submit
92
+ needs: [version, release]
93
+ # Runs after a skipped upload too (the version is already with Apple), never after a
94
+ # failed one.
95
+ if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/ios-v') && needs.release.result != 'failure' }}
96
+ runs-on: ubuntu-latest
97
+ timeout-minutes: 60
98
+ permissions:
99
+ contents: write # create/update the GitHub release for the tag
100
+ env:
101
+ ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
102
+ ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
103
+ SUBMIT: ${{ needs.version.outputs.submitted != 'true' }}
104
+ steps:
105
+ - uses: actions/checkout@v7
106
+ with:
107
+ fetch-depth: 0 # git rev-list --count HEAD must see the full history
108
+
109
+ - uses: ruby/setup-ruby@v1
110
+ if: env.SUBMIT == 'true'
111
+ with:
112
+ ruby-version: '3.4.11'
113
+ bundler-cache: true
114
+ working-directory: infra
115
+
116
+ - name: Install the App Store Connect API key
117
+ if: env.SUBMIT == 'true'
118
+ run: |
119
+ set -euo pipefail
120
+ mkdir -p "$RUNNER_TEMP/asc"
121
+ printf '%s' "${{ secrets.ASC_KEY_P8 }}" > "$RUNNER_TEMP/asc/AuthKey.p8"
122
+ echo "ASC_KEY_PATH=$RUNNER_TEMP/asc/AuthKey.p8" >> "$GITHUB_ENV"
123
+
124
+ - name: Compute version numbers
125
+ run: |
126
+ set -euo pipefail
127
+ echo "BUILD=$(git rev-list --count HEAD)" >> "$GITHUB_ENV"
128
+ echo "MARKETING=${GITHUB_REF_NAME#ios-v}" >> "$GITHUB_ENV"
129
+
130
+ - name: Publish the GitHub release with the full changelog
131
+ env:
132
+ GH_TOKEN: ${{ github.token }}
133
+ run: |
134
+ set -euo pipefail
135
+ # Full (untruncated) bullets of the first "## " section — GitHub has no length cap.
136
+ NOTES="$RUNNER_TEMP/gh-release-notes.md"
137
+ awk '/^## /{n++; if (n==2) exit} n==1 && /^- /{print}' CHANGELOG.md > "$NOTES"
138
+ if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
139
+ gh release edit "$GITHUB_REF_NAME" --title "iOS $MARKETING" --notes-file "$NOTES"
140
+ else
141
+ gh release create "$GITHUB_REF_NAME" --title "iOS $MARKETING" --notes-file "$NOTES"
142
+ fi
143
+
144
+ - name: Build the "What's New" notes for every App Store locale
145
+ if: env.SUBMIT == 'true'
146
+ run: |
147
+ set -euo pipefail
148
+ # One release_notes.txt per declared App Store locale. App Store Connect asks for
149
+ # "What's New" in each of them, and a locale deliver does not write keeps whatever
150
+ # it had — which on a new version is nothing. One pair per listing of
151
+ # infra/fastlane/metadata/: notes for a locale with no listing would make
152
+ # deliver create it with no description and no screenshots, and the
153
+ # submission would be refused. Add a locale here when its listing exists.
154
+ #
155
+ # Bullets under the first "## " section become the notes. App Store caps them at
156
+ # 4000 chars, so keep whole bullets until ~3900 and drop the rest.
157
+ for pair in \
158
+ "fr-FR:CHANGELOG.fr.md" \
159
+ "en-US:CHANGELOG.md"
160
+ do
161
+ locale="${pair%%:*}"
162
+ source="${pair#*:}"
163
+ mkdir -p "infra/fastlane/metadata/$locale"
164
+ awk '/^## /{n++; if (n==2) exit} n==1 && /^- /{print}' "$source" \
165
+ | awk 'BEGIN{max=3900; total=0} {len=length($0)+1; if (total+len>max) exit; total+=len; print}' \
166
+ > "infra/fastlane/metadata/$locale/release_notes.txt"
167
+ if [ ! -s "infra/fastlane/metadata/$locale/release_notes.txt" ]; then
168
+ echo "::error::No release notes for $locale — $source has no bullet under its first ## heading"
169
+ exit 1
170
+ fi
171
+ echo "---- What's New ($locale from $source), $(wc -c < "infra/fastlane/metadata/$locale/release_notes.txt") chars ----"
172
+ cat "infra/fastlane/metadata/$locale/release_notes.txt"
173
+ done
174
+
175
+ - name: Attach build and submit for review
176
+ if: env.SUBMIT == 'true'
177
+ working-directory: infra
178
+ run: bundle exec fastlane ios submit
@@ -0,0 +1,57 @@
1
+ name: TestFlight
2
+
3
+ # Every night, puts main on the internal TestFlight group ("Nightly" in App Store Connect,
4
+ # which has access to every build), so a change can be tried on a phone without tagging a
5
+ # release that is not one. The build also goes to the external "Public" group through Beta
6
+ # App Review, never to App Review. When main has not moved since the last upload, the build
7
+ # already exists and ios-upload.yml stops before any macOS job.
8
+
9
+ on:
10
+ schedule:
11
+ - cron: '0 1 * * *' # 03:00 in Paris in summer, 02:00 in winter
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ # Shared with release-ios.yml: two uploads at once would race for the same build number.
18
+ concurrency:
19
+ group: ios-upload
20
+ cancel-in-progress: false
21
+
22
+ jobs:
23
+ version:
24
+ name: 🏷️ Version
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 5
27
+ outputs:
28
+ marketing: ${{ steps.version.outputs.marketing }}
29
+ steps:
30
+ - uses: actions/checkout@v7
31
+ with:
32
+ fetch-depth: 0 # the ios-v* tags decide whether a version has shipped
33
+
34
+ # The version the next release will carry. The latest versioned heading of
35
+ # CHANGELOG.md is that release until its tag exists; once it has shipped, App Store
36
+ # Connect closes its train to new builds, so the nightly moves on to the next minor.
37
+ - name: Pick the version under development
38
+ id: version
39
+ run: |
40
+ set -euo pipefail
41
+ LATEST=$(grep -m1 -oE '^## [0-9]+(\.[0-9]+)+' CHANGELOG.md | cut -c4- || true)
42
+ if [ -z "$LATEST" ]; then
43
+ MARKETING=1.0
44
+ elif git rev-parse -q --verify "refs/tags/ios-v$LATEST" >/dev/null; then
45
+ MARKETING=$(echo "$LATEST" | awk -F. '{ print $1 "." $2 + 1 }')
46
+ else
47
+ MARKETING="$LATEST"
48
+ fi
49
+ echo "marketing=$MARKETING" >> "$GITHUB_OUTPUT"
50
+ echo "Nightly build of {{APP_NAME}} $MARKETING"
51
+
52
+ testflight:
53
+ needs: version
54
+ uses: ./.github/workflows/ios-upload.yml
55
+ with:
56
+ marketing_version: ${{ needs.version.outputs.marketing }}
57
+ secrets: inherit
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Used only by CI (release-ios.yml `submit` job) to attach the uploaded build to its
4
+ # App Store version and submit it for review. Not needed for local development.
5
+ gem "fastlane"
@@ -0,0 +1 @@
1
+ app_identifier("{{BUNDLE_ID}}")
@@ -0,0 +1,54 @@
1
+ default_platform(:ios)
2
+
3
+ platform :ios do
4
+ # Screenshots are not uploaded from here: `deliver` once put every panel on a version
5
+ # twice, on a run it reported as successful. They go up from the Mac, on request.
6
+
7
+ # Attach the already-uploaded build to its App Store version, set the "What's New"
8
+ # notes from CHANGELOG.md, and submit for review with automatic release on approval.
9
+ # The binary is uploaded by the `release` job (altool); this lane never uploads it.
10
+ desc "Attach the uploaded build to its version and submit it for review (auto-release)."
11
+ lane :submit do
12
+ api_key = app_store_connect_api_key(
13
+ key_id: ENV.fetch("ASC_KEY_ID"),
14
+ issuer_id: ENV.fetch("ASC_ISSUER_ID"),
15
+ key_filepath: ENV.fetch("ASC_KEY_PATH")
16
+ )
17
+
18
+ app_id = "{{BUNDLE_ID}}"
19
+ version = ENV.fetch("MARKETING")
20
+ build = ENV.fetch("BUILD")
21
+
22
+ # App Store Connect needs several minutes to finish processing an uploaded build before
23
+ # it can be attached to a version — until then deliver raises "Build number: N does not
24
+ # exist". Retry the whole submit (metadata upload is idempotent) until the build is
25
+ # attachable, giving up after ~30 min.
26
+ attempts = 0
27
+ begin
28
+ deliver(
29
+ api_key: api_key,
30
+ app_identifier: app_id,
31
+ app_version: version,
32
+ build_number: build,
33
+ submit_for_review: true,
34
+ automatic_release: true,
35
+ force: true, # no interactive HTML preview
36
+ skip_binary_upload: true, # the binary was already uploaded by the release job
37
+ skip_screenshots: true, # screenshots are managed manually, once
38
+ run_precheck_before_submit: false,
39
+ submission_information: {
40
+ export_compliance_uses_encryption: false,
41
+ add_id_info_uses_idfa: false
42
+ }
43
+ )
44
+ rescue => e
45
+ attempts += 1
46
+ if attempts <= 30 && e.message.to_s.match?(/does not exist|still.*process|not.*process/i)
47
+ UI.message("Build #{build} not processed yet (attempt #{attempts}/30) — waiting 60s…")
48
+ sleep(60)
49
+ retry
50
+ end
51
+ raise
52
+ end
53
+ end
54
+ end
@@ -0,0 +1 @@
1
+ 2026 Polyforms
@@ -0,0 +1 @@
1
+ {{APP_NAME}}
@@ -0,0 +1 @@
1
+ https://{{APP_SLUG}}.polyforms.co/privacy.html
@@ -0,0 +1 @@
1
+ {{APP_TAGLINE}}
@@ -0,0 +1 @@
1
+ https://{{APP_SLUG}}.polyforms.co
@@ -0,0 +1 @@
1
+ {{APP_NAME}}
@@ -0,0 +1 @@
1
+ https://{{APP_SLUG}}.polyforms.co/privacy.html
@@ -0,0 +1 @@
1
+ {{APP_TAGLINE}}
@@ -0,0 +1 @@
1
+ https://{{APP_SLUG}}.polyforms.co