@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.
- package/CHANGELOG.md +18 -0
- package/README.md +26 -2
- package/bin/stack.ts +109 -0
- package/hooks/shutdown-simulators-after-push.sh +19 -0
- package/package.json +40 -4
- package/routines/daily-health.md +78 -0
- package/routines/deprecations.md +23 -0
- package/routines/sentry-autofix.md +76 -0
- package/routines/stack-upgrade.md +45 -0
- package/rules/00-collaboration.md +81 -0
- package/rules/01-workflow.md +81 -0
- package/rules/02-commits.md +71 -0
- package/rules/03-code-review.md +25 -0
- package/rules/04-push.md +75 -0
- package/rules/05-release.md +40 -0
- package/rules/06-changelog.md +48 -0
- package/rules/07-appstore.md +77 -0
- package/rules/08-copy.md +52 -0
- package/rules/09-backend.md +132 -0
- package/rules/10-ios.md +206 -0
- package/rules/11-ops.md +114 -0
- package/rules/12-ci.md +32 -0
- package/rules/13-ai.md +71 -0
- package/rules/14-infra.md +90 -0
- package/rules/15-tooling.md +81 -0
- package/rules/16-routines.md +69 -0
- package/rules/17-landing.md +76 -0
- package/skills/appstore/SKILL.md +53 -0
- package/skills/domain/SKILL.md +136 -0
- package/skills/graphql/SKILL.md +83 -0
- package/skills/ios-feature/SKILL.md +94 -0
- package/skills/stack-install/SKILL.md +35 -0
- package/skills/stack-upgrade/SKILL.md +27 -0
- package/src/catalogue.ts +33 -0
- package/src/config.ts +20 -0
- package/src/init.ts +63 -0
- package/src/migrations.ts +72 -0
- package/src/migrations.unit.test.ts +44 -0
- package/src/package.ts +8 -0
- package/src/render.ts +13 -0
- package/src/render.unit.test.ts +17 -0
- package/src/sync.int.test.ts +103 -0
- package/src/sync.ts +161 -0
- package/src/templates.ts +121 -0
- package/src/validate-content.ts +65 -0
- package/templates/backend/.env.example +10 -0
- package/templates/backend/.github/workflows/deploy.yml +151 -0
- package/templates/backend/.github/workflows/schema.yml +59 -0
- package/templates/backend/.github/workflows/test-feat.yml +40 -0
- package/templates/backend/.github/workflows/test-int.yml +40 -0
- package/templates/backend/.github/workflows/test-unit.yml +64 -0
- package/templates/backend/docs/api-evolution.md +67 -0
- package/templates/backend/firebase.json +21 -0
- package/templates/backend/firestore.indexes.json +4 -0
- package/templates/backend/firestore.rules +11 -0
- package/templates/backend/infra/auth.tf +54 -0
- package/templates/backend/infra/backend.tf +6 -0
- package/templates/backend/infra/cicd.tf +63 -0
- package/templates/backend/infra/firebase.tf +26 -0
- package/templates/backend/infra/firestore.tf +53 -0
- package/templates/backend/infra/function.tf +171 -0
- package/templates/backend/infra/main.tf +26 -0
- package/templates/backend/infra/observer.tf +18 -0
- package/templates/backend/infra/outputs.tf +30 -0
- package/templates/backend/infra/project.tf +64 -0
- package/templates/backend/infra/scheduler.tf +28 -0
- package/templates/backend/infra/secrets.tf +44 -0
- package/templates/backend/infra/state.tf +13 -0
- package/templates/backend/infra/terraform.tfvars.example +16 -0
- package/templates/backend/infra/variables.tf +104 -0
- package/templates/backend/infra/versions.tf +30 -0
- package/templates/backend/nitro.config.ts +40 -0
- package/templates/backend/package.json +51 -0
- package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
- package/templates/backend/scripts/deprecations/status.ts +50 -0
- package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
- package/templates/backend/scripts/deprecations/usage.ts +79 -0
- package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
- package/templates/backend/scripts/deprecations.ts +224 -0
- package/templates/backend/scripts/export-graphql-schema.ts +11 -0
- package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
- package/templates/backend/scripts/install-terraform.sh +61 -0
- package/templates/backend/scripts/tf +11 -0
- package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
- package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
- package/templates/backend/server/domain/changelog/primitives.ts +19 -0
- package/templates/backend/server/domain/changelog/query.ts +13 -0
- package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
- package/templates/backend/server/domain/changelog/types.ts +9 -0
- package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
- package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
- package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
- package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
- package/templates/backend/server/domain/shared/language.ts +26 -0
- package/templates/backend/server/domain/shared/page.ts +25 -0
- package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
- package/templates/backend/server/domain/shared/primitives.ts +50 -0
- package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
- package/templates/backend/server/domain/shared/types.ts +14 -0
- package/templates/backend/server/domain/user/command.ts +16 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
- package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
- package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
- package/templates/backend/server/domain/user/query.int.test.ts +49 -0
- package/templates/backend/server/domain/user/query.ts +34 -0
- package/templates/backend/server/domain/user/types.ts +20 -0
- package/templates/backend/server/domain/user/use-case.ts +18 -0
- package/templates/backend/server/middleware/auth.ts +54 -0
- package/templates/backend/server/plugins/00-sentry.ts +74 -0
- package/templates/backend/server/plugins/01-request-log.ts +28 -0
- package/templates/backend/server/plugins/02-graphql.ts +69 -0
- package/templates/backend/server/plugins/03-boot-time.ts +21 -0
- package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
- package/templates/backend/server/routes/app-config.get.ts +15 -0
- package/templates/backend/server/routes/graphql.ts +52 -0
- package/templates/backend/server/system/app-support.ts +17 -0
- package/templates/backend/server/system/config/index.ts +18 -0
- package/templates/backend/server/system/config/primitives.ts +20 -0
- package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
- package/templates/backend/server/system/config/types.ts +8 -0
- package/templates/backend/server/system/firebase.ts +19 -0
- package/templates/backend/server/system/identity.ts +18 -0
- package/templates/backend/server/system/logger.ts +119 -0
- package/templates/backend/server/system/logger.unit.test.ts +109 -0
- package/templates/backend/server/system/migration/migrations/index.ts +6 -0
- package/templates/backend/server/system/migration/primitives.ts +18 -0
- package/templates/backend/server/system/migration/runner.ts +52 -0
- package/templates/backend/server/system/migration/types.ts +16 -0
- package/templates/backend/server/system/read-meter.ts +100 -0
- package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
- package/templates/backend/server/system/request-cache.ts +139 -0
- package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
- package/templates/backend/server/system/secret-box.ts +49 -0
- package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
- package/templates/backend/server/test/bdd.ts +12 -0
- package/templates/backend/server/test/fake-firestore.ts +483 -0
- package/templates/backend/server/utils/apollo.ts +13 -0
- package/templates/backend/server/utils/fetch.ts +5 -0
- package/templates/backend/server/utils/firestore.ts +91 -0
- package/templates/backend/server/utils/input.ts +36 -0
- package/templates/backend/server/utils/input.unit.test.ts +43 -0
- package/templates/backend/server/utils/slug.ts +21 -0
- package/templates/backend/tsconfig.json +16 -0
- package/templates/base/.github/workflows/renovate.yml +17 -0
- package/templates/base/CHANGELOG.fr.md +3 -0
- package/templates/base/CHANGELOG.md +7 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/README.md +12 -0
- package/templates/base/biome.json +47 -0
- package/templates/base/bunfig.toml +2 -0
- package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
- package/templates/base/docs/data-recovery.md +116 -0
- package/templates/base/docs/ideas.md +4 -0
- package/templates/base/project-context.md +43 -0
- package/templates/base/renovate.json +37 -0
- package/templates/ios/.github/workflows/build-ios.yml +168 -0
- package/templates/ios/.github/workflows/ios-upload.yml +308 -0
- package/templates/ios/.github/workflows/release-ios.yml +178 -0
- package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
- package/templates/ios/infra/Gemfile +5 -0
- package/templates/ios/infra/fastlane/Appfile +1 -0
- package/templates/ios/infra/fastlane/Fastfile +54 -0
- package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
- package/templates/ios/infra/ios.tf +28 -0
- package/templates/ios/ios/ExportOptions.plist +26 -0
- package/templates/ios/ios/apollo-codegen-config.json +16 -0
- package/templates/ios/ios/project.yml +94 -0
- package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
- package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
- package/templates/ios/package.merge.json +10 -0
- package/templates/ios/scripts/apollo-codegen.ts +31 -0
- package/templates/ios/scripts/check-ios-components.ts +72 -0
- package/templates/ios/scripts/check-strings.ts +103 -0
- package/templates/ios/scripts/install-device.sh +29 -0
- 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 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{APP_NAME}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://{{APP_SLUG}}.polyforms.co/privacy.html
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{APP_TAGLINE}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://{{APP_SLUG}}.polyforms.co
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{APP_NAME}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://{{APP_SLUG}}.polyforms.co/privacy.html
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{APP_TAGLINE}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://{{APP_SLUG}}.polyforms.co
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UTILITIES
|