@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,21 @@
|
|
|
1
|
+
const LEADING_ARTICLES = /^(the|a|an|le|la|les|l|un|une|des|du|de)[\s-]+/
|
|
2
|
+
|
|
3
|
+
/** Folds a title, a saga name or an author into a key two spellings of the same
|
|
4
|
+
* thing agree on. Diacritics are dropped, punctuation collapses to a separator
|
|
5
|
+
* and a leading article is removed, so "L'Assassin royal" and "Assassin Royal"
|
|
6
|
+
* meet.
|
|
7
|
+
*
|
|
8
|
+
* Shared because two different things are keyed on it and they must agree: the
|
|
9
|
+
* series catalogue's document id, which two readers have to converge on, and the
|
|
10
|
+
* match that tells an import a book is already on the reader's shelves. */
|
|
11
|
+
export const slugify = (value: string): string =>
|
|
12
|
+
value
|
|
13
|
+
.normalize('NFD')
|
|
14
|
+
.replace(/[̀-ͯ]/g, '')
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/['’]/g, ' ')
|
|
17
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(LEADING_ARTICLES, '')
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/\s+/g, '-')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.nitro/types/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"strict": true,
|
|
5
|
+
"noImplicitAny": true,
|
|
6
|
+
"noImplicitReturns": true,
|
|
7
|
+
"noFallthroughCasesInSwitch": true,
|
|
8
|
+
"useUnknownInCatchVariables": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"types": ["nitropack", "bun-types"],
|
|
14
|
+
"lib": ["ESNext", "DOM"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Renovate
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 20 * * *' # 22h CET / 21h UTC
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
renovate:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
- uses: renovatebot/github-action@v46.3.9
|
|
14
|
+
with:
|
|
15
|
+
token: ${{ secrets.RENOVATE_TOKEN }}
|
|
16
|
+
env:
|
|
17
|
+
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
English is the source of truth. Every served language has its own file, kept in lockstep:
|
|
4
|
+
`CHANGELOG.fr.md`. Only what a user notices is logged, in their words.
|
|
5
|
+
|
|
6
|
+
Each version heading carries the App Store version and its release date: `## 1.0 (2026.10.01)`.
|
|
7
|
+
Entries are written when an App Store release is cut, never ahead of it.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
<!-- stack:rules -->
|
|
4
|
+
<!-- /stack:rules -->
|
|
5
|
+
|
|
6
|
+
What the app is, in two sentences, for someone who has never opened it.
|
|
7
|
+
|
|
8
|
+
The rules above are the Polyforms stack, version `{{STACK_VERSION}}` (`stack/VERSION`), and
|
|
9
|
+
this file holds only what is specific to this project. The project's facts (ids, accounts,
|
|
10
|
+
devices, certificates, pending work) are in [project-context.md](project-context.md).
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
| Purpose | Command |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Typecheck | `bun run typecheck` |
|
|
17
|
+
| Tests, by family | `bun run test:unit`, `bun run test:int`, `bun run test:feat` |
|
|
18
|
+
| Preflight (all of the above plus lint) | `bun run preflight` |
|
|
19
|
+
| Linter | `bun run lint` / `bun run lint:fix` |
|
|
20
|
+
| GraphQL schema | `bun run generate:graphql` |
|
|
21
|
+
| Schema compatibility | `bun run schema:check` |
|
|
22
|
+
| Deprecations | `bun run deprecations` |
|
|
23
|
+
| Dev server | `bun run dev` |
|
|
24
|
+
| Stack in sync | `bun run stack:check` |
|
|
25
|
+
|
|
26
|
+
## Data model
|
|
27
|
+
|
|
28
|
+
What is private and what is shared, the collections and their keys, the invariants a
|
|
29
|
+
reviewer must know before touching a domain. Written once the first domain exists.
|
|
30
|
+
|
|
31
|
+
## Traps of this project
|
|
32
|
+
|
|
33
|
+
One line per trap, with the symptom and the way around it. Anything that would hold in
|
|
34
|
+
another project goes into the stack instead of here.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
What the app does, for someone who has never opened it. Why before how, numbered steps, no
|
|
4
|
+
deep architecture and no screenshot that ages.
|
|
5
|
+
|
|
6
|
+
## Run it
|
|
7
|
+
|
|
8
|
+
1. `bun install`
|
|
9
|
+
2. Copy `.env.example` to `.env` and fill it.
|
|
10
|
+
3. `bun run dev` serves the API on http://localhost:3000 (GraphQL at `POST /graphql`).
|
|
11
|
+
|
|
12
|
+
Built on the Polyforms stack (`stack/VERSION`).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true,
|
|
10
|
+
"includes": [
|
|
11
|
+
"**",
|
|
12
|
+
"!shared",
|
|
13
|
+
"!site",
|
|
14
|
+
"!stack",
|
|
15
|
+
"!stack.config.json",
|
|
16
|
+
"!server/system/changelog-content.ts",
|
|
17
|
+
"!ios/**/Assets.xcassets",
|
|
18
|
+
"!ios/**/*.xcstrings"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"formatter": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"indentStyle": "space",
|
|
24
|
+
"indentWidth": 2,
|
|
25
|
+
"lineWidth": 100
|
|
26
|
+
},
|
|
27
|
+
"linter": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"rules": {
|
|
30
|
+
"preset": "recommended",
|
|
31
|
+
"correctness": {
|
|
32
|
+
"noUnusedImports": "error",
|
|
33
|
+
"noUnusedVariables": "error"
|
|
34
|
+
},
|
|
35
|
+
"style": {
|
|
36
|
+
"noNamespace": "error"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"javascript": {
|
|
41
|
+
"formatter": {
|
|
42
|
+
"quoteStyle": "single",
|
|
43
|
+
"semicolons": "asNeeded",
|
|
44
|
+
"trailingCommas": "all"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 0001. Built on the Polyforms stack
|
|
2
|
+
|
|
3
|
+
Date: {{TODAY}}
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Every Polyforms app shares one way of being built: a Bun and Nitro backend on Firebase,
|
|
8
|
+
GraphQL through Pothos, Terraform, one CI workflow per concern, Sentry, scheduled routines,
|
|
9
|
+
and for apps with a phone client, SwiftUI with the stack's conventions.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
This project consumes the `@moifort/stack` package. Its rules are rendered into `stack/`
|
|
14
|
+
and imported by `CLAUDE.md`; its templates seeded the repository; a version bump arrives as
|
|
15
|
+
a Renovate pull request and is applied by the upgrade routine.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
A rule is never changed here: a correction that holds for every project goes into the
|
|
20
|
+
package and comes back as a version. What is specific to this project is written in
|
|
21
|
+
`CLAUDE.md`, `project-context.md` and the ADRs that follow this one.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Data recovery — Firestore protections and runbook
|
|
2
|
+
|
|
3
|
+
What protects production data, and what to do when it is lost. Read this before touching
|
|
4
|
+
anything: a hurried restore over live writes does more damage than the incident.
|
|
5
|
+
|
|
6
|
+
## What is in place
|
|
7
|
+
|
|
8
|
+
Both settings live on `google_firestore_database.default` in `infra/firebase.tf`, applied by
|
|
9
|
+
the Deploy workflow (since 2026-10-19, commit 5dc4f24).
|
|
10
|
+
|
|
11
|
+
| Protection | Covers | Retention |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Delete protection (`DELETE_PROTECTION_ENABLED`) | Deleting the `(default)` database from the console, `gcloud` or a script | — |
|
|
14
|
+
| Point-in-time recovery (`POINT_IN_TIME_RECOVERY_ENABLED`) | Documents deleted or overwritten by a bug, a migration, a bad command | 7 days, to the minute |
|
|
15
|
+
| Terraform `deletion_policy = ABANDON` (provider default) | `terraform destroy` leaves the database alone | — |
|
|
16
|
+
| GCP project soft delete (platform default) | Deleting the whole project | 30 days |
|
|
17
|
+
|
|
18
|
+
**Not in place, on purpose:** scheduled Firestore backups, an export to a bucket in another
|
|
19
|
+
project, a lien on the project. So there is **no copy of the data outside the database
|
|
20
|
+
itself**: a deleted database or a project gone past 30 days is unrecoverable.
|
|
21
|
+
|
|
22
|
+
Check the live state (read-only, works with the observer account):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
CLOUDSDK_CONFIG=$HOME/.config/{{APP_SLUG}}-observer/gcloud gcloud firestore databases describe --database='(default)' --project={{APP_SLUG}}-polyforms --format='value(deleteProtectionState,pointInTimeRecoveryEnablement,earliestVersionTime,versionRetentionPeriod)'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`earliestVersionTime` is the oldest instant that can still be recovered.
|
|
29
|
+
|
|
30
|
+
## Before any recovery
|
|
31
|
+
|
|
32
|
+
1. **Stop the bleeding first.** If a bug or a migration is still writing, revert and deploy
|
|
33
|
+
the fix (or pause the scheduler job) before restoring, or the restore is overwritten.
|
|
34
|
+
2. **Pin the instant.** Find the last whole minute *before* the damage, from Cloud Logging,
|
|
35
|
+
the `migration-meta` collection or the deploy time. PITR snapshot times must be a whole
|
|
36
|
+
minute (`…:05:00Z`) and later than `earliestVersionTime`. The clock runs: past seven days
|
|
37
|
+
the version is gone, so do this the day the damage is seen.
|
|
38
|
+
3. **Scope the damage.** Which collections, which `userId`s, which documents. Collections are
|
|
39
|
+
flat with a `userId` field, so a reader's library is one query.
|
|
40
|
+
4. Recovery writes production: it needs the user's own `gcloud` login (the observer account is
|
|
41
|
+
read-only) and **the user's go-ahead**. It is never done silently.
|
|
42
|
+
|
|
43
|
+
## Case 1 — documents deleted or overwritten, less than 7 days ago
|
|
44
|
+
|
|
45
|
+
Never restore the whole database over `(default)`: it would erase every legitimate write made
|
|
46
|
+
since. Recover only what was damaged.
|
|
47
|
+
|
|
48
|
+
**A handful of documents** — read the old version straight from `(default)` with a read-only
|
|
49
|
+
transaction at a past `readTime`, then write it back:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { Timestamp } from 'firebase-admin/firestore'
|
|
53
|
+
|
|
54
|
+
const readTime = Timestamp.fromDate(new Date('2026-10-19T14:05:00Z'))
|
|
55
|
+
const before = await db.runTransaction((tx) => tx.get(db.collection('books').doc(bookId)), {
|
|
56
|
+
readOnly: true,
|
|
57
|
+
readTime,
|
|
58
|
+
})
|
|
59
|
+
if (before.exists) await db.collection('books').doc(bookId).set(before.data()!)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A query works the same way (`tx.get(db.collection('books').where('userId', '==', userId))`).
|
|
63
|
+
|
|
64
|
+
**Many documents or whole collections** — clone the database at that instant into a
|
|
65
|
+
separate database, copy back what was damaged, then delete the clone:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
gcloud firestore databases clone --project={{APP_SLUG}}-polyforms --source-database='projects/{{APP_SLUG}}-polyforms/databases/(default)' --snapshot-time=2026-10-19T14:05:00Z --destination-database=recovered
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then a one-off script (in `scripts/`, run locally with the user's credentials) opens both
|
|
72
|
+
databases — `getFirestore(app)` and `getFirestore(app, 'recovered')` — and copies back the
|
|
73
|
+
damaged documents, in batches of at most 500 writes. A document deleted by the incident
|
|
74
|
+
exists in `recovered` and not in `(default)`; one created since the incident exists only in
|
|
75
|
+
`(default)` and must be left alone.
|
|
76
|
+
|
|
77
|
+
Delete the clone when done, it bills storage while it exists:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gcloud firestore databases delete --database=recovered --project={{APP_SLUG}}-polyforms
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If `clone` is refused (flag names change; check `gcloud firestore databases clone --help`),
|
|
84
|
+
the fallback is an export at the same instant into a bucket, then an import into a new
|
|
85
|
+
database: `gcloud firestore export gs://<bucket>/recovery --snapshot-time=… --database='(default)'`,
|
|
86
|
+
create `recovered`, `gcloud firestore import gs://<bucket>/recovery --database=recovered`.
|
|
87
|
+
|
|
88
|
+
## Case 2 — the `(default)` database deleted
|
|
89
|
+
|
|
90
|
+
Delete protection refuses this; someone has to turn it off first
|
|
91
|
+
(`gcloud firestore databases update --database='(default)' --no-delete-protection`, or the
|
|
92
|
+
console). If it happened anyway, the PITR history went with the database and **there is no
|
|
93
|
+
backup to restore from**. What is left is to recreate the database by applying Terraform
|
|
94
|
+
(the Deploy workflow) and to tell readers. Shared series catalogues rebuild themselves from
|
|
95
|
+
new scans; private books do not.
|
|
96
|
+
|
|
97
|
+
## Case 3 — the whole GCP project deleted
|
|
98
|
+
|
|
99
|
+
A deleted project stays in "pending deletion" for 30 days: everything is shut down, nothing is
|
|
100
|
+
erased. Restore it, with the user's own login (owner):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
gcloud projects undelete {{APP_SLUG}}-polyforms
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then check billing is still linked (the prepaid Gemini key and Cloud Run need it), that Cloud
|
|
107
|
+
Run serves again, and that Firestore answers. APIs and services come back with the project;
|
|
108
|
+
re-run the Deploy workflow if anything is missing. Past 30 days the project, its data and its
|
|
109
|
+
ID are gone for good.
|
|
110
|
+
|
|
111
|
+
## If more protection is ever wanted
|
|
112
|
+
|
|
113
|
+
From cheapest to strongest: a lien on the project (free, blocks project deletion until
|
|
114
|
+
removed); a `google_firestore_backup_schedule` (daily kept 7 days, weekly kept 14 weeks,
|
|
115
|
+
billed as backup storage, restore into a new database); a scheduled export to a bucket in
|
|
116
|
+
another project with a retention lock — the only copy that survives losing the project.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Project context
|
|
2
|
+
|
|
3
|
+
The facts of this project. Nothing here is a rule (rules live in the stack) and nothing
|
|
4
|
+
here should be copied into another project.
|
|
5
|
+
|
|
6
|
+
## Environments
|
|
7
|
+
|
|
8
|
+
| Environment | GCP project | Notes |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| Production | `{{APP_SLUG}}-polyforms` | |
|
|
11
|
+
| Staging | `{{APP_SLUG}}-staging-polyforms` | Deployed from pull requests labelled `staging` |
|
|
12
|
+
|
|
13
|
+
## Accounts and consoles
|
|
14
|
+
|
|
15
|
+
Which account opens which console, and the access trick if there is one.
|
|
16
|
+
|
|
17
|
+
## Analytics and monitoring
|
|
18
|
+
|
|
19
|
+
Sentry projects `{{APP_SLUG}}-node` and `{{APP_SLUG}}-ios` (org `polyforms`). GA4 property, dashboards.
|
|
20
|
+
|
|
21
|
+
## Test devices
|
|
22
|
+
|
|
23
|
+
Name, identifier, signing team, trust state.
|
|
24
|
+
|
|
25
|
+
## Certificates and keys
|
|
26
|
+
|
|
27
|
+
| Item | Role | Created | Expires |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| | Signing | | |
|
|
30
|
+
| | App Store Connect API | | |
|
|
31
|
+
|
|
32
|
+
## Where each secret lives
|
|
33
|
+
|
|
34
|
+
Every token and the places it is replicated, with the example file beside each ignored one.
|
|
35
|
+
|
|
36
|
+
## Decisions specific to this project
|
|
37
|
+
|
|
38
|
+
Structuring choices and their reason, when neither the code nor the history says it. An
|
|
39
|
+
architecture choice gets an ADR in `docs/adr/`.
|
|
40
|
+
|
|
41
|
+
## Pending work
|
|
42
|
+
|
|
43
|
+
What is started, what remains to be done by hand, what waits on someone else.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended"],
|
|
4
|
+
"pinDigests": false,
|
|
5
|
+
"gitAuthor": "Renovate Bot <{{GITHUB_OWNER}}@users.noreply.github.com>",
|
|
6
|
+
"labels": ["dependencies"],
|
|
7
|
+
"packageRules": [
|
|
8
|
+
{
|
|
9
|
+
"description": "Automerge non-major updates in a single grouped PR",
|
|
10
|
+
"matchUpdateTypes": ["minor", "patch", "digest"],
|
|
11
|
+
"groupName": "non-major dependencies",
|
|
12
|
+
"commitMessageTopic": "dependencies",
|
|
13
|
+
"automerge": true,
|
|
14
|
+
"automergeType": "pr",
|
|
15
|
+
"automergeStrategy": "squash"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"description": "Major updates: one pull request each, never automerged; the upgrade routine reads the changelog, applies the code changes, deploys to staging and merges",
|
|
19
|
+
"matchUpdateTypes": ["major"],
|
|
20
|
+
"labels": ["dependencies", "major"],
|
|
21
|
+
"automerge": false
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"description": "The stack package: never automerged; the upgrade routine applies its migrations and merges",
|
|
25
|
+
"matchPackageNames": ["@moifort/stack"],
|
|
26
|
+
"labels": ["dependencies", "stack"],
|
|
27
|
+
"automerge": false
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"lockFileMaintenance": {
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"schedule": ["before 5am on monday"],
|
|
33
|
+
"automerge": true,
|
|
34
|
+
"automergeType": "pr",
|
|
35
|
+
"automergeStrategy": "squash"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
# Runs only for what the app is built from: its sources, the committed schema the
|
|
4
|
+
# Apollo code is regenerated from (so a backend change that breaks an iOS operation
|
|
5
|
+
# still lands here), the two checks the job ends with, and this file. A server-only
|
|
6
|
+
# push no longer pays twenty minutes of macOS runner, nor cancels a running build.
|
|
7
|
+
# No branch protection requires this check, so a run that never starts blocks nothing.
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
paths:
|
|
12
|
+
- 'ios/**'
|
|
13
|
+
- 'shared/schema.graphql'
|
|
14
|
+
- 'scripts/check-strings.ts'
|
|
15
|
+
- 'scripts/check-ios-components.ts'
|
|
16
|
+
- '.github/workflows/build-ios.yml'
|
|
17
|
+
pull_request:
|
|
18
|
+
branches: [main]
|
|
19
|
+
paths:
|
|
20
|
+
- 'ios/**'
|
|
21
|
+
- 'shared/schema.graphql'
|
|
22
|
+
- 'scripts/check-strings.ts'
|
|
23
|
+
- 'scripts/check-ios-components.ts'
|
|
24
|
+
- '.github/workflows/build-ios.yml'
|
|
25
|
+
workflow_dispatch:
|
|
26
|
+
|
|
27
|
+
# A newer push to the same branch, or to the same pull request, makes this run
|
|
28
|
+
# pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
|
|
29
|
+
# the workflow file and the ref, since the three test workflows share one name.
|
|
30
|
+
concurrency:
|
|
31
|
+
group: ${{ github.workflow_ref }}
|
|
32
|
+
cancel-in-progress: true
|
|
33
|
+
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
# Guards the one thing the other three workflows cannot see: whether the app
|
|
39
|
+
# still compiles. The generated Apollo code is regenerated here from the
|
|
40
|
+
# committed schema, so a backend change that the iOS operations no longer match
|
|
41
|
+
# fails here rather than at release time. Builds without signing or uploading.
|
|
42
|
+
# macos-26 is a standard runner, so this costs nothing on a public repository.
|
|
43
|
+
ios:
|
|
44
|
+
name: 📱 iOS
|
|
45
|
+
runs-on: macos-26
|
|
46
|
+
# The simulator's first boot on a fresh runner takes ten minutes and more,
|
|
47
|
+
# on top of a cold build: 30 cut a run short on 2026-10-16.
|
|
48
|
+
timeout-minutes: 45
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v7
|
|
51
|
+
|
|
52
|
+
- name: Select the latest final Xcode
|
|
53
|
+
uses: maxim-lobanov/setup-xcode@v1
|
|
54
|
+
with:
|
|
55
|
+
xcode-version: latest-stable
|
|
56
|
+
|
|
57
|
+
# Booted first, so its ten-minute first start runs alongside the Apollo
|
|
58
|
+
# generation and the build rather than after them, where it sat as eleven
|
|
59
|
+
# silent minutes before the tests on 2026-10-16.
|
|
60
|
+
#
|
|
61
|
+
# The iPhone 17 precisely: the scheme's test pre-action boots "iPhone 17" to
|
|
62
|
+
# add the UI tests' photo. On any other device that pre-action cold-booted a
|
|
63
|
+
# second simulator inside the test step, six to fifteen silent minutes before
|
|
64
|
+
# the first test on 2026-10-18–20. simctl resolves the name to the device
|
|
65
|
+
# already booted, so here it finds this one ready and does nothing.
|
|
66
|
+
- name: Boot the simulator the tests run on
|
|
67
|
+
run: |
|
|
68
|
+
set -euo pipefail
|
|
69
|
+
devices=$(xcrun simctl list devices available)
|
|
70
|
+
line=$(grep -m1 -E '^ +iPhone 17 \(' <<< "$devices" \
|
|
71
|
+
|| grep -m1 -E '^ +iPhone' <<< "$devices")
|
|
72
|
+
udid=$(grep -oE '[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}' <<< "$line")
|
|
73
|
+
echo "Testing on $line"
|
|
74
|
+
echo "SIMULATOR_UDID=$udid" >> "$GITHUB_ENV"
|
|
75
|
+
xcrun simctl boot "$udid"
|
|
76
|
+
|
|
77
|
+
- name: Generate the Xcode project
|
|
78
|
+
run: |
|
|
79
|
+
brew install xcodegen
|
|
80
|
+
(cd ios && xcodegen generate)
|
|
81
|
+
|
|
82
|
+
- name: Materialize gitignored build files
|
|
83
|
+
run: |
|
|
84
|
+
set -euo pipefail
|
|
85
|
+
printf '%s' "${{ secrets.IOS_GOOGLE_SERVICE_INFO_PLIST }}" \
|
|
86
|
+
| openssl base64 -d -A > ios/{{APP_NAME}}/GoogleService-Info.plist
|
|
87
|
+
cp ios/{{APP_NAME}}/Shared/Secrets.swift.example ios/{{APP_NAME}}/Shared/Secrets.swift
|
|
88
|
+
|
|
89
|
+
# Fetching the packages took four of the Apollo step's minutes on 2026-10-20.
|
|
90
|
+
# Package.resolved is not committed, so the cache cannot be keyed on it before
|
|
91
|
+
# resolving: the latest cache is restored, resolution then fetches only what
|
|
92
|
+
# moved, and a new cache is saved only when the resolved versions changed.
|
|
93
|
+
- name: Restore the Swift packages
|
|
94
|
+
id: spm-restore
|
|
95
|
+
uses: actions/cache/restore@v6
|
|
96
|
+
with:
|
|
97
|
+
path: ${{ runner.temp }}/SourcePackages
|
|
98
|
+
key: spm-${{ runner.os }}-${{ github.sha }}
|
|
99
|
+
restore-keys: spm-${{ runner.os }}-
|
|
100
|
+
|
|
101
|
+
- name: Generate Apollo GraphQL code (gitignored, regenerated per build)
|
|
102
|
+
run: |
|
|
103
|
+
set -euo pipefail
|
|
104
|
+
# Resolve SPM so the apollo-ios checkout (and its bundled CLI) exists.
|
|
105
|
+
xcodebuild -resolvePackageDependencies \
|
|
106
|
+
-project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
|
|
107
|
+
-derivedDataPath "$RUNNER_TEMP/DerivedData" \
|
|
108
|
+
-clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages"
|
|
109
|
+
resolved=ios/{{APP_NAME}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
|
|
110
|
+
if [ -f "$resolved" ]; then
|
|
111
|
+
echo "SPM_CACHE_KEY=spm-${RUNNER_OS}-$(shasum -a 256 "$resolved" | cut -c1-16)" \
|
|
112
|
+
>> "$GITHUB_ENV"
|
|
113
|
+
fi
|
|
114
|
+
# Apollo 2.x ships the CLI as a tarball inside the checkout — extract it.
|
|
115
|
+
CLI_DIR="$RUNNER_TEMP/SourcePackages/checkouts/apollo-ios/CLI"
|
|
116
|
+
tar -xzf "$CLI_DIR/apollo-ios-cli.tar.gz" -C "$CLI_DIR"
|
|
117
|
+
chmod +x "$CLI_DIR/apollo-ios-cli"
|
|
118
|
+
(cd ios && "$CLI_DIR/apollo-ios-cli" generate)
|
|
119
|
+
|
|
120
|
+
- name: Save the Swift packages
|
|
121
|
+
if: env.SPM_CACHE_KEY != '' && steps.spm-restore.outputs.cache-matched-key != env.SPM_CACHE_KEY
|
|
122
|
+
uses: actions/cache/save@v6
|
|
123
|
+
with:
|
|
124
|
+
path: ${{ runner.temp }}/SourcePackages
|
|
125
|
+
key: ${{ env.SPM_CACHE_KEY }}
|
|
126
|
+
|
|
127
|
+
- name: Build the app and its tests
|
|
128
|
+
# Compiled once, for the simulator the tests then run on: no signing
|
|
129
|
+
# identity needed (release-ios.yml owns those).
|
|
130
|
+
run: |
|
|
131
|
+
set -euo pipefail
|
|
132
|
+
xcodebuild -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
|
|
133
|
+
-destination "id=$SIMULATOR_UDID" \
|
|
134
|
+
-derivedDataPath "$RUNNER_TEMP/DerivedData" \
|
|
135
|
+
-clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \
|
|
136
|
+
CODE_SIGNING_ALLOWED=NO \
|
|
137
|
+
build-for-testing
|
|
138
|
+
|
|
139
|
+
- name: Unit tests
|
|
140
|
+
# The ViewModels driven over inline clients ({{APP_NAME}}Tests): no network, no
|
|
141
|
+
# Firebase, only a simulator to host the bundle. Runs what the step above
|
|
142
|
+
# built, without compiling anything again.
|
|
143
|
+
run: |
|
|
144
|
+
set -euo pipefail
|
|
145
|
+
# Says how long the boot still had to go, so a slow one shows here.
|
|
146
|
+
start=$SECONDS
|
|
147
|
+
xcrun simctl bootstatus "$SIMULATOR_UDID" -b
|
|
148
|
+
echo "Simulator ready after a further $((SECONDS - start)) s"
|
|
149
|
+
xcodebuild -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
|
|
150
|
+
-destination "id=$SIMULATOR_UDID" \
|
|
151
|
+
-derivedDataPath "$RUNNER_TEMP/DerivedData" \
|
|
152
|
+
-clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \
|
|
153
|
+
-only-testing:{{APP_NAME}}Tests \
|
|
154
|
+
CODE_SIGNING_ALLOWED=NO \
|
|
155
|
+
test-without-building
|
|
156
|
+
|
|
157
|
+
- uses: oven-sh/setup-bun@v2
|
|
158
|
+
|
|
159
|
+
- name: Every string has its French and its English
|
|
160
|
+
# Reads the strings the build just extracted: one added to the code and
|
|
161
|
+
# left out of the catalogue fails here, as does one with no English.
|
|
162
|
+
run: bun scripts/check-strings.ts "$RUNNER_TEMP/DerivedData"
|
|
163
|
+
|
|
164
|
+
- name: Components stay pure and previewed
|
|
165
|
+
# Stack rule 10-ios.md: nothing under components/ or Shared/Components
|
|
166
|
+
# calls the server, reads a singleton or holds a ViewModel,
|
|
167
|
+
# and every view there has a #Preview.
|
|
168
|
+
run: bun scripts/check-ios-components.ts
|