@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,10 @@
|
|
|
1
|
+
# Bearer token guarding /admin/* (migrations, scheduled routes). Any string locally.
|
|
2
|
+
NITRO_ADMIN_TOKEN=your-admin-token
|
|
3
|
+
# Sentry DSN of the <app>-node project. Blank disables reporting; never set in dev.
|
|
4
|
+
NITRO_SENTRY_DSN=
|
|
5
|
+
# Sentry user auth token, for the local error triage tooling only (read + resolve
|
|
6
|
+
# issues through the REST API). Not a runtime setting.
|
|
7
|
+
SENTRY_AUTH_TOKEN=
|
|
8
|
+
# Dev-only: the Firebase UID the auth bypass signs requests as, so Apollo Sandbox
|
|
9
|
+
# works without a token.
|
|
10
|
+
NITRO_DEV_USER_ID=
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
# Every push to main redeploys, except one that touches only the landing page,
|
|
4
|
+
# the docs or the captures: none of them reaches the backend, and the Pages
|
|
5
|
+
# workflow publishes the site on its own. The backend is never applied from a
|
|
6
|
+
# workstation: the Terraform state lives in GCS and this job is its only writer.
|
|
7
|
+
# A pull request labelled `staging` deploys its branch to the staging project.
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
paths-ignore:
|
|
12
|
+
- 'site/**'
|
|
13
|
+
- 'docs/**'
|
|
14
|
+
- 'screenshots/**'
|
|
15
|
+
- 'README.md'
|
|
16
|
+
- 'CLAUDE.md'
|
|
17
|
+
pull_request:
|
|
18
|
+
types: [labeled, synchronize]
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
id-token: write # required for Workload Identity Federation
|
|
24
|
+
|
|
25
|
+
# The Terraform state takes a lock, so two applies at once do not interleave.
|
|
26
|
+
# Queue the runs instead of racing them, and never cancel one mid-apply.
|
|
27
|
+
concurrency:
|
|
28
|
+
group: deploy-${{ github.event_name == 'pull_request' && 'staging' || 'production' }}
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
deploy:
|
|
33
|
+
name: 🚀 Backend (${{ github.event_name == 'pull_request' && 'staging' || 'production' }})
|
|
34
|
+
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'staging')
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
timeout-minutes: 20
|
|
37
|
+
env:
|
|
38
|
+
# One workspace per environment: the staging project is the same Terraform
|
|
39
|
+
# with its own state prefix and variables.
|
|
40
|
+
TF_WORKSPACE: ${{ github.event_name == 'pull_request' && 'staging' || 'default' }}
|
|
41
|
+
PROJECT_ID: ${{ github.event_name == 'pull_request' && vars.GCP_STAGING_PROJECT_ID || vars.GCP_PROJECT_ID }}
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v7
|
|
44
|
+
with:
|
|
45
|
+
fetch-depth: 0 # sentry-cli reads the commit history to attach it to the release
|
|
46
|
+
|
|
47
|
+
# A missing secret reaches Terraform as an empty string, not as null: an unset
|
|
48
|
+
# ADMIN_TOKEN would leave /admin/* unguarded. Fail before anything is applied.
|
|
49
|
+
- name: Check the deploy secrets are set
|
|
50
|
+
env:
|
|
51
|
+
GCP_WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER }}
|
|
52
|
+
GCP_DEPLOY_SA: ${{ secrets.GCP_DEPLOY_SA }}
|
|
53
|
+
GCP_BILLING_ACCOUNT: ${{ secrets.GCP_BILLING_ACCOUNT }}
|
|
54
|
+
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
|
|
55
|
+
GCP_ORG_ID: ${{ vars.GCP_ORG_ID }}
|
|
56
|
+
run: |
|
|
57
|
+
missing=0
|
|
58
|
+
for name in GCP_WIF_PROVIDER GCP_DEPLOY_SA GCP_BILLING_ACCOUNT ADMIN_TOKEN GCP_ORG_ID PROJECT_ID; do
|
|
59
|
+
if [ -z "${!name}" ]; then
|
|
60
|
+
echo "::error::$name is not set"
|
|
61
|
+
missing=1
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
exit "$missing"
|
|
65
|
+
|
|
66
|
+
- uses: oven-sh/setup-bun@v2
|
|
67
|
+
- uses: hashicorp/setup-terraform@v4
|
|
68
|
+
with:
|
|
69
|
+
terraform_version: v1.16.6
|
|
70
|
+
|
|
71
|
+
- name: Authenticate to Google Cloud (workload identity)
|
|
72
|
+
uses: google-github-actions/auth@v3
|
|
73
|
+
with:
|
|
74
|
+
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
|
|
75
|
+
service_account: ${{ secrets.GCP_DEPLOY_SA }}
|
|
76
|
+
|
|
77
|
+
- name: Build the Nitro bundle (firebase preset)
|
|
78
|
+
env:
|
|
79
|
+
# Baked into the bundle, so every error Sentry receives says which
|
|
80
|
+
# deploy produced it and which source maps decode it.
|
|
81
|
+
SENTRY_RELEASE: {{APP_SLUG}}-node@${{ github.sha }}
|
|
82
|
+
run: |
|
|
83
|
+
bun install --frozen-lockfile
|
|
84
|
+
bun run generate:assets
|
|
85
|
+
bunx nitro prepare
|
|
86
|
+
bun run generate:graphql
|
|
87
|
+
bun run build
|
|
88
|
+
|
|
89
|
+
# The update screen sends every build below the floor to the App Store: a floor
|
|
90
|
+
# above the build on sale would block users with nothing to install.
|
|
91
|
+
- name: Check the build floor is on the App Store
|
|
92
|
+
env:
|
|
93
|
+
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
94
|
+
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
95
|
+
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
96
|
+
run: bun scripts/deprecations.ts check-floor
|
|
97
|
+
|
|
98
|
+
- name: Upload the source maps to Sentry
|
|
99
|
+
env:
|
|
100
|
+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
101
|
+
SENTRY_ORG: polyforms
|
|
102
|
+
SENTRY_PROJECT: {{APP_SLUG}}-node
|
|
103
|
+
SENTRY_RELEASE: {{APP_SLUG}}-node@${{ github.sha }}
|
|
104
|
+
run: |
|
|
105
|
+
set -euo pipefail
|
|
106
|
+
if [ -n "$SENTRY_AUTH_TOKEN" ]; then
|
|
107
|
+
curl -sL https://sentry.io/get-cli/ | bash
|
|
108
|
+
sentry-cli releases new "$SENTRY_RELEASE"
|
|
109
|
+
sentry-cli sourcemaps inject .output/server
|
|
110
|
+
sentry-cli sourcemaps upload --release "$SENTRY_RELEASE" .output/server
|
|
111
|
+
sentry-cli releases set-commits "$SENTRY_RELEASE" --local --ignore-missing
|
|
112
|
+
sentry-cli releases finalize "$SENTRY_RELEASE"
|
|
113
|
+
else
|
|
114
|
+
echo "::warning::SENTRY_AUTH_TOKEN is unset, deploying without source maps"
|
|
115
|
+
fi
|
|
116
|
+
# The maps themselves never ship: Terraform zips this directory as-is.
|
|
117
|
+
find .output/server -name '*.map' -delete
|
|
118
|
+
|
|
119
|
+
- name: Materialize terraform.tfvars from GitHub secrets
|
|
120
|
+
run: |
|
|
121
|
+
set -euo pipefail
|
|
122
|
+
printf '%s' "${{ secrets.APPLE_PRIVATE_KEY_P8 }}" > infra/apple.p8
|
|
123
|
+
chmod 600 infra/apple.p8
|
|
124
|
+
cat > infra/terraform.tfvars <<EOF
|
|
125
|
+
project_id = "${PROJECT_ID}"
|
|
126
|
+
billing_account_id = "${{ secrets.GCP_BILLING_ACCOUNT }}"
|
|
127
|
+
org_id = "${{ vars.GCP_ORG_ID }}"
|
|
128
|
+
region = "europe-west3"
|
|
129
|
+
firestore_location = "eur3"
|
|
130
|
+
apple_team_id = "${{ secrets.APPLE_TEAM_ID }}"
|
|
131
|
+
apple_services_id = "${{ secrets.APPLE_SERVICES_ID }}"
|
|
132
|
+
apple_key_id = "${{ secrets.APPLE_KEY_ID }}"
|
|
133
|
+
apple_private_key_path = "${{ secrets.APPLE_PRIVATE_KEY_P8 != '' && './apple.p8' || '' }}"
|
|
134
|
+
sentry_dsn = "${{ secrets.SENTRY_DSN }}"
|
|
135
|
+
admin_token = "${{ secrets.ADMIN_TOKEN }}"
|
|
136
|
+
observer_service_account = "${{ vars.OBSERVER_SERVICE_ACCOUNT }}"
|
|
137
|
+
EOF
|
|
138
|
+
|
|
139
|
+
- name: Terraform init + apply
|
|
140
|
+
working-directory: infra
|
|
141
|
+
run: |
|
|
142
|
+
terraform init -input=false
|
|
143
|
+
terraform apply -auto-approve -input=false
|
|
144
|
+
|
|
145
|
+
- name: Run Firestore migrations
|
|
146
|
+
working-directory: infra
|
|
147
|
+
run: |
|
|
148
|
+
URL=$(terraform output -raw function_url)
|
|
149
|
+
curl -fsS -X POST \
|
|
150
|
+
-H "Authorization: Bearer ${{ secrets.ADMIN_TOKEN }}" \
|
|
151
|
+
"${URL}/admin/migrate"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Schema
|
|
2
|
+
|
|
3
|
+
# A field or an argument leaves the GraphQL schema only after it was deprecated:
|
|
4
|
+
# builds on the App Store may still ask for anything else. See docs/api-evolution.md.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- 'site/**'
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- 'screenshots/**'
|
|
12
|
+
- 'README.md'
|
|
13
|
+
- 'CLAUDE.md'
|
|
14
|
+
pull_request:
|
|
15
|
+
branches: [main]
|
|
16
|
+
paths-ignore:
|
|
17
|
+
- 'site/**'
|
|
18
|
+
- 'docs/**'
|
|
19
|
+
- 'screenshots/**'
|
|
20
|
+
- 'README.md'
|
|
21
|
+
- 'CLAUDE.md'
|
|
22
|
+
|
|
23
|
+
# A newer push to the same branch, or to the same pull request, makes this run
|
|
24
|
+
# pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
|
|
25
|
+
# the workflow file and the ref, since the three test workflows share one name.
|
|
26
|
+
concurrency:
|
|
27
|
+
group: ${{ github.workflow_ref }}
|
|
28
|
+
cancel-in-progress: true
|
|
29
|
+
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
breaking:
|
|
35
|
+
name: 🧬 No breaking change without deprecation
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0 # the diff reads the schema at the base commit
|
|
41
|
+
- uses: oven-sh/setup-bun@v2
|
|
42
|
+
- run: bun install --frozen-lockfile
|
|
43
|
+
# The base is what main held before this push, or the target of the pull request.
|
|
44
|
+
# A first push has no before (all zeros): there is nothing to compare against.
|
|
45
|
+
- name: Compare the schema with its base
|
|
46
|
+
env:
|
|
47
|
+
BEFORE: ${{ github.event.before }}
|
|
48
|
+
BASE_REF: ${{ github.base_ref }}
|
|
49
|
+
run: |
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
if [ -n "$BASE_REF" ]; then
|
|
52
|
+
base="origin/$BASE_REF"
|
|
53
|
+
elif [ -z "${BEFORE//0/}" ]; then
|
|
54
|
+
echo "No previous commit to compare with"
|
|
55
|
+
exit 0
|
|
56
|
+
else
|
|
57
|
+
base="$BEFORE"
|
|
58
|
+
fi
|
|
59
|
+
SCHEMA_BASE="$base" bun run schema:check
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- 'site/**'
|
|
8
|
+
- 'docs/**'
|
|
9
|
+
- 'screenshots/**'
|
|
10
|
+
- 'README.md'
|
|
11
|
+
- 'CLAUDE.md'
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
paths-ignore:
|
|
15
|
+
- 'site/**'
|
|
16
|
+
- 'docs/**'
|
|
17
|
+
- 'screenshots/**'
|
|
18
|
+
- 'README.md'
|
|
19
|
+
- 'CLAUDE.md'
|
|
20
|
+
|
|
21
|
+
# A newer push to the same branch, or to the same pull request, makes this run
|
|
22
|
+
# pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
|
|
23
|
+
# the workflow file and the ref, since the three test workflows share one name.
|
|
24
|
+
concurrency:
|
|
25
|
+
group: ${{ github.workflow_ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
feat:
|
|
33
|
+
name: 🎯 Feature
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
- uses: oven-sh/setup-bun@v2
|
|
38
|
+
- run: bun install --frozen-lockfile
|
|
39
|
+
- run: bunx nitro prepare
|
|
40
|
+
- run: bun test .feat.test
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- 'site/**'
|
|
8
|
+
- 'docs/**'
|
|
9
|
+
- 'screenshots/**'
|
|
10
|
+
- 'README.md'
|
|
11
|
+
- 'CLAUDE.md'
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
paths-ignore:
|
|
15
|
+
- 'site/**'
|
|
16
|
+
- 'docs/**'
|
|
17
|
+
- 'screenshots/**'
|
|
18
|
+
- 'README.md'
|
|
19
|
+
- 'CLAUDE.md'
|
|
20
|
+
|
|
21
|
+
# A newer push to the same branch, or to the same pull request, makes this run
|
|
22
|
+
# pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
|
|
23
|
+
# the workflow file and the ref, since the three test workflows share one name.
|
|
24
|
+
concurrency:
|
|
25
|
+
group: ${{ github.workflow_ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
int:
|
|
33
|
+
name: 🔗 Integration
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
- uses: oven-sh/setup-bun@v2
|
|
38
|
+
- run: bun install --frozen-lockfile
|
|
39
|
+
- run: bunx nitro prepare
|
|
40
|
+
- run: bun test .int.test
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- 'site/**'
|
|
8
|
+
- 'docs/**'
|
|
9
|
+
- 'screenshots/**'
|
|
10
|
+
- 'README.md'
|
|
11
|
+
- 'CLAUDE.md'
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
paths-ignore:
|
|
15
|
+
- 'site/**'
|
|
16
|
+
- 'docs/**'
|
|
17
|
+
- 'screenshots/**'
|
|
18
|
+
- 'README.md'
|
|
19
|
+
- 'CLAUDE.md'
|
|
20
|
+
|
|
21
|
+
# A newer push to the same branch, or to the same pull request, makes this run
|
|
22
|
+
# pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
|
|
23
|
+
# the workflow file and the ref, since the three test workflows share one name.
|
|
24
|
+
concurrency:
|
|
25
|
+
group: ${{ github.workflow_ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
unit:
|
|
33
|
+
name: 🧪 Unit
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
- uses: oven-sh/setup-bun@v2
|
|
38
|
+
- run: bun install --frozen-lockfile
|
|
39
|
+
- run: bunx nitro prepare
|
|
40
|
+
- run: bun tsc --noEmit
|
|
41
|
+
- run: bunx biome check
|
|
42
|
+
- run: bun test .unit.test
|
|
43
|
+
|
|
44
|
+
# Guard: the Cloud Function build on GCP runs `npm install` (strict peer deps)
|
|
45
|
+
# on the package.json Nitro generates in .output/server. bun is lenient about
|
|
46
|
+
# peer deps, so an incompatible bump passes locally and only fails at
|
|
47
|
+
# `terraform apply`. This reproduces that resolution here so it fails fast.
|
|
48
|
+
deploy-deps:
|
|
49
|
+
name: 📦 Prod deps (npm strict)
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v7
|
|
53
|
+
- uses: oven-sh/setup-bun@v2
|
|
54
|
+
- uses: actions/setup-node@v7
|
|
55
|
+
with:
|
|
56
|
+
node-version: 24
|
|
57
|
+
- run: bun install --frozen-lockfile
|
|
58
|
+
- run: bun run build
|
|
59
|
+
- name: Validate generated function deps under npm (strict peer deps)
|
|
60
|
+
working-directory: .output/server
|
|
61
|
+
# --dry-run builds the full dependency tree (so ERESOLVE peer-dep
|
|
62
|
+
# conflicts still surface) without the reify/write step, which avoids an
|
|
63
|
+
# unrelated npm arborist crash ("fsTop") on the runner.
|
|
64
|
+
run: npm install --dry-run --ignore-scripts
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# API evolution — deprecation and forced update
|
|
2
|
+
|
|
3
|
+
The GraphQL schema is not versioned. Builds of the app stay installed for weeks after a
|
|
4
|
+
release, so the schema only ever grows, and what the app stops using leaves it in two steps:
|
|
5
|
+
first deprecated, then removed once a build without it has been on the App Store for two
|
|
6
|
+
weeks — and in that same commit, the builds that still ask for it are sent to update.
|
|
7
|
+
|
|
8
|
+
The design is [the deprecation routine spec](superpowers/specs/2026-09-27-deprecation-routine-design.md).
|
|
9
|
+
|
|
10
|
+
## Moving parts
|
|
11
|
+
|
|
12
|
+
| Piece | File |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Build floor and App Store URL | `server/system/app-support.ts` (`MINIMUM_SUPPORTED_IOS_BUILD`) |
|
|
15
|
+
| Public config route (`GET /app-config`) | `server/routes/app-config.get.ts`, whitelisted in `server/middleware/auth.ts` |
|
|
16
|
+
| Launch and foreground check, fail-open, skipped in Debug | `ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift` |
|
|
17
|
+
| Blocking screen | `ios/{{APP_NAME}}/Shared/Components/UpdateRequiredView.swift`, wired in `AuthRoot.swift` |
|
|
18
|
+
| What can go, and the floor to set | `scripts/deprecations.ts` (`bun run deprecations`) |
|
|
19
|
+
| No breaking change without deprecation | `bun run schema:check`, the Schema workflow |
|
|
20
|
+
| The floor never outruns the App Store | `bun scripts/deprecations.ts check-floor`, in the Deploy workflow |
|
|
21
|
+
|
|
22
|
+
The comparison key is the build number (`CFBundleVersion`), which CI sets to
|
|
23
|
+
`git rev-list --count HEAD`: it names a commit and only grows, nightly or App Store alike.
|
|
24
|
+
Never the marketing version.
|
|
25
|
+
|
|
26
|
+
## What breaks an installed build
|
|
27
|
+
|
|
28
|
+
Removing or renaming a field, an argument or an input field; changing a type; making an
|
|
29
|
+
argument or an input field required; making a non-null output field nullable. Adding a field,
|
|
30
|
+
a query, a mutation, an optional argument or an enum value does not — the app reads an enum
|
|
31
|
+
value it does not know as a neutral one (`Book+GraphQL.swift`).
|
|
32
|
+
|
|
33
|
+
## The routine
|
|
34
|
+
|
|
35
|
+
1. **Deprecate, in the commit where the app stops asking.** The iOS operations
|
|
36
|
+
(`ios/**/*.graphql`) stop selecting the field or argument, and the server marks it with
|
|
37
|
+
`deprecationReason`, naming what replaces it. The resolver keeps working.
|
|
38
|
+
2. **At every App Store release, ask what can go:** `bun run deprecations` (with
|
|
39
|
+
`ASC_KEY_ID`, `ASC_ISSUER_ID` and `ASC_KEY_PATH` set to the team key). For each deprecated
|
|
40
|
+
element it finds, in git, the first build whose operations no longer select it, and
|
|
41
|
+
compares it with the build on sale:
|
|
42
|
+
- **removable** — a build without it has been on the App Store for fourteen days, counted
|
|
43
|
+
from the `ios-v<version>` tag; or no build ever asked for it;
|
|
44
|
+
- **in grace until `<date>`** — on sale, not for fourteen days yet;
|
|
45
|
+
- **waiting for a release** — the App Store still sells a build that asks for it;
|
|
46
|
+
- **still asked by the app** — an operation at `HEAD` selects it;
|
|
47
|
+
- **check by hand** — an input field or an enum value, which the operations cannot show:
|
|
48
|
+
Swift fills inputs through variables, and enum values travel in responses.
|
|
49
|
+
3. **Remove, in one commit:** the removable elements — schema, resolver, the code and tests
|
|
50
|
+
only they used — and `MINIMUM_SUPPORTED_IOS_BUILD` set to the floor the script proposes.
|
|
51
|
+
The Deploy workflow ships both together; an older build shows the update screen at its next
|
|
52
|
+
launch or return to the foreground.
|
|
53
|
+
|
|
54
|
+
## Guards
|
|
55
|
+
|
|
56
|
+
- `bun run schema:check` compares `shared/schema.graphql` with `origin/main` (or
|
|
57
|
+
`SCHEMA_BASE`) through `graphql-inspector`. It fails on any breaking change except the
|
|
58
|
+
removal of a field or an argument that was deprecated — the second through
|
|
59
|
+
`scripts/deprecations/inspector-rule.ts`, since the built-in rule spares fields only.
|
|
60
|
+
- `check-floor` fails the deploy when the floor is above the build on sale: readers would be
|
|
61
|
+
blocked with no update to install. A floor of `1` passes without calling Apple.
|
|
62
|
+
|
|
63
|
+
## Fail-open
|
|
64
|
+
|
|
65
|
+
If `/app-config` is unreachable or undecodable, the app stays usable. A build that slips past
|
|
66
|
+
the gate meets GraphQL errors until its next check — never lock readers out because the check
|
|
67
|
+
itself failed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"firestore": {
|
|
3
|
+
"rules": "firestore.rules",
|
|
4
|
+
"indexes": "firestore.indexes.json"
|
|
5
|
+
},
|
|
6
|
+
"functions": [
|
|
7
|
+
{
|
|
8
|
+
"source": ".output/server",
|
|
9
|
+
"codebase": "server",
|
|
10
|
+
"runtime": "nodejs22",
|
|
11
|
+
"ignore": ["node_modules"]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"emulators": {
|
|
15
|
+
"auth": { "port": 9099 },
|
|
16
|
+
"functions": { "port": 5001 },
|
|
17
|
+
"firestore": { "port": 8080 },
|
|
18
|
+
"ui": { "enabled": true, "port": 4000 },
|
|
19
|
+
"singleProjectMode": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
rules_version = '2';
|
|
2
|
+
|
|
3
|
+
// All Firestore access goes through Cloud Functions (Application Default
|
|
4
|
+
// Credentials bypass these rules). Direct client access is denied.
|
|
5
|
+
service cloud.firestore {
|
|
6
|
+
match /databases/{database}/documents {
|
|
7
|
+
match /{document=**} {
|
|
8
|
+
allow read, write: if false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
locals {
|
|
2
|
+
apple_sign_in_configured = var.apple_services_id != "" && var.apple_key_id != "" && var.apple_private_key_path != ""
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
resource "google_identity_platform_config" "this" {
|
|
6
|
+
provider = google-beta
|
|
7
|
+
project = google_project.this.project_id
|
|
8
|
+
autodelete_anonymous_users = false
|
|
9
|
+
|
|
10
|
+
sign_in {
|
|
11
|
+
allow_duplicate_emails = false
|
|
12
|
+
|
|
13
|
+
email {
|
|
14
|
+
enabled = false
|
|
15
|
+
password_required = false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
anonymous {
|
|
19
|
+
enabled = false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
depends_on = [google_firebase_project.this]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Apple Sign-In, configured as a built-in (default supported) provider.
|
|
27
|
+
# The iOS SDK calls OAuthProvider.appleCredential(...) which targets the
|
|
28
|
+
# provider id "apple.com", so a custom OIDC config (oidc.apple.com) would
|
|
29
|
+
# never be matched and produces "identity provider configuration is not
|
|
30
|
+
# found" at runtime.
|
|
31
|
+
#
|
|
32
|
+
# Identity Platform accepts the Apple key material as a JSON-encoded
|
|
33
|
+
# client_secret; it generates the OAuth JWT internally on each sign-in.
|
|
34
|
+
resource "google_identity_platform_default_supported_idp_config" "apple" {
|
|
35
|
+
# Skipped entirely when the Apple credentials are blank. The provider cannot be
|
|
36
|
+
# half-configured: Identity Platform rejects an empty client_id, and a resource
|
|
37
|
+
# created with placeholder key material would fail at the first sign-in rather
|
|
38
|
+
# than at apply time, which is the worse place to find out.
|
|
39
|
+
count = local.apple_sign_in_configured ? 1 : 0
|
|
40
|
+
|
|
41
|
+
provider = google-beta
|
|
42
|
+
project = google_project.this.project_id
|
|
43
|
+
enabled = true
|
|
44
|
+
idp_id = "apple.com"
|
|
45
|
+
client_id = var.apple_services_id
|
|
46
|
+
|
|
47
|
+
client_secret = jsonencode({
|
|
48
|
+
teamId = var.apple_team_id
|
|
49
|
+
keyId = var.apple_key_id
|
|
50
|
+
privateKey = file(var.apple_private_key_path)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
depends_on = [google_identity_platform_config.this]
|
|
54
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Workload Identity Federation: lets the GitHub Actions deploy workflow
|
|
2
|
+
# authenticate to GCP without a long-lived service account key. The flow is:
|
|
3
|
+
#
|
|
4
|
+
# 1. GitHub Actions exchanges its OIDC token at the WIF provider below.
|
|
5
|
+
# 2. GCP returns a short-lived token that impersonates the deploy SA.
|
|
6
|
+
# 3. Terraform runs apply against this project as that SA.
|
|
7
|
+
#
|
|
8
|
+
# Outputs (deploy_sa_email + wif_provider) are surfaced through outputs.tf
|
|
9
|
+
# so they can be copied into GitHub secrets after a fresh bootstrap.
|
|
10
|
+
|
|
11
|
+
resource "google_service_account" "deploy" {
|
|
12
|
+
project = google_project.this.project_id
|
|
13
|
+
account_id = "{{APP_SLUG}}-deploy"
|
|
14
|
+
display_name = "{{APP_NAME}} GitHub Actions deploy"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Project-scoped owner: the deploy workflow runs `terraform apply`, which
|
|
18
|
+
# touches every resource in this project (project services, IAM, Cloud Run,
|
|
19
|
+
# Functions, Firestore, Secret Manager, GCS, org policy). Owner is the
|
|
20
|
+
# simplest contract that survives future resource additions; the impact is
|
|
21
|
+
# bounded because the binding is on a single project, not the org.
|
|
22
|
+
resource "google_project_iam_member" "deploy_owner" {
|
|
23
|
+
project = google_project.this.project_id
|
|
24
|
+
role = "roles/owner"
|
|
25
|
+
member = "serviceAccount:${google_service_account.deploy.email}"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resource "google_iam_workload_identity_pool" "github" {
|
|
29
|
+
project = google_project.this.project_id
|
|
30
|
+
workload_identity_pool_id = "github"
|
|
31
|
+
display_name = "GitHub Actions"
|
|
32
|
+
description = "OIDC federation for GitHub-hosted runners"
|
|
33
|
+
|
|
34
|
+
depends_on = [google_project_service.apis]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource "google_iam_workload_identity_pool_provider" "github" {
|
|
38
|
+
project = google_project.this.project_id
|
|
39
|
+
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
|
|
40
|
+
workload_identity_pool_provider_id = "github-actions"
|
|
41
|
+
display_name = "GitHub OIDC"
|
|
42
|
+
|
|
43
|
+
attribute_mapping = {
|
|
44
|
+
"google.subject" = "assertion.sub"
|
|
45
|
+
"attribute.actor" = "assertion.actor"
|
|
46
|
+
"attribute.repository" = "assertion.repository"
|
|
47
|
+
"attribute.ref" = "assertion.ref"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Mandatory since 2023: scope the provider to a single repository so that
|
|
51
|
+
# a leaked workflow on another repo cannot impersonate the deploy SA.
|
|
52
|
+
attribute_condition = "assertion.repository == '${var.github_repo}'"
|
|
53
|
+
|
|
54
|
+
oidc {
|
|
55
|
+
issuer_uri = "https://token.actions.githubusercontent.com"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
resource "google_service_account_iam_member" "deploy_wif_user" {
|
|
60
|
+
service_account_id = google_service_account.deploy.name
|
|
61
|
+
role = "roles/iam.workloadIdentityUser"
|
|
62
|
+
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.github_repo}"
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
resource "google_firebase_project" "this" {
|
|
2
|
+
provider = google-beta
|
|
3
|
+
project = google_project.this.project_id
|
|
4
|
+
|
|
5
|
+
depends_on = [google_project_service.apis]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
resource "google_firestore_database" "default" {
|
|
9
|
+
provider = google-beta
|
|
10
|
+
project = google_project.this.project_id
|
|
11
|
+
name = "(default)"
|
|
12
|
+
location_id = var.firestore_location
|
|
13
|
+
type = "FIRESTORE_NATIVE"
|
|
14
|
+
concurrency_mode = "OPTIMISTIC"
|
|
15
|
+
app_engine_integration_mode = "DISABLED"
|
|
16
|
+
|
|
17
|
+
# A console click or a stray script cannot delete the database: protection must be
|
|
18
|
+
# turned off first, on purpose.
|
|
19
|
+
delete_protection_state = "DELETE_PROTECTION_ENABLED"
|
|
20
|
+
|
|
21
|
+
# Keeps every document version for seven days, readable and restorable to the minute,
|
|
22
|
+
# so a failed migration or a bug that overwrites books can be undone.
|
|
23
|
+
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
|
|
24
|
+
|
|
25
|
+
depends_on = [google_firebase_project.this]
|
|
26
|
+
}
|