diff --git a/.claude/skills/run-on-simulator/SKILL.md b/.claude/skills/run-on-simulator/SKILL.md index 4c77ceb440..ef85444836 100644 --- a/.claude/skills/run-on-simulator/SKILL.md +++ b/.claude/skills/run-on-simulator/SKILL.md @@ -268,6 +268,8 @@ prebuild — needs `APP_VARIANT`; app.config.ts has no default. | `aao-dev` | `NFMTHAZVS9.com.drewvolz.stolaf.dev` | | `carls` | `com.rives.carls` | | `carls-dev` | `com.rives.carls.dev` | +| `aaa` | `tech.frogpond.allaboutanything` | +| `aaa-dev` | `tech.frogpond.allaboutanything.dev` | The URL scheme matches the bundle id, so a dev-variant build wants `…stolaf.dev://…`. Two builds claiming one scheme is undefined behaviour, diff --git a/AGENTS.md b/AGENTS.md index 61df6e2bdf..8e818d3b8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,16 +174,21 @@ workflow sets its own in its environment. | `aao-dev` | `…stolaf.dev` | AAO Dev | `all-about-olaf` | | `carls` | `com.rives.carls` | CARLS | `carls` | | `carls-dev` | `com.rives.carls.dev` | CARLS Dev | `carls` | +| `aaa` | `tech.frogpond.allaboutanything` | All About Anything | `all-about-anything` | +| `aaa-dev` | `…allaboutanything.dev` | AAA Dev | `all-about-anything` | The two All About Olaf variants share the windmill icon, so tell them apart by name. The CARLS variants build the same code as Carleton's app: `extra.defaultCampus` opens them on Carleton (`source/lib/app-identity.ts`), the penguin is the primary icon, and the windmills are its alternates. `com.rives.carls` is the CARLS app's own identifier, so a release build updates CARLS on the App Store. +All About Anything opens on the campus picker, wears Windmill (Sky), and alone +answers `allaboutanything://`. ```bash mise run aao:ios [device] # AAO Dev, prebuilt and run mise run carls:ios [device] # CARLS Dev, prebuilt and run +mise run aaa:ios [device] # AAA Dev, prebuilt and run ``` `ios/` holds one variant at a time; `mise run prebuild` starts it afresh when diff --git a/__tests__/app.config.test.ts b/__tests__/app.config.test.ts index d3323a0a59..dbece6634a 100644 --- a/__tests__/app.config.test.ts +++ b/__tests__/app.config.test.ts @@ -59,7 +59,7 @@ describe('app.config variants', () => { // Unset builds nothing, so a forgotten variant cannot launch the other app. it.each([undefined, ''])('refuses to build with APP_VARIANT %p', (variant) => { expect(() => loadConfig(variant)).toThrow( - /APP_VARIANT is not set.*aao, aao-dev, carls, carls-dev/u, + /APP_VARIANT is not set.*aao, aao-dev, carls, carls-dev, aaa, aaa-dev/u, ) }) @@ -73,7 +73,7 @@ describe('app.config variants', () => { // `name` also names the generated Xcode project, its target, its scheme and // its directory. It must not vary per variant: every plugin looks the // AllAboutAnything target up by name. - it.each(['aao', 'aao-dev', 'carls', 'carls-dev'])( + it.each(['aao', 'aao-dev', 'carls', 'carls-dev', 'aaa', 'aaa-dev'])( 'keeps the Xcode project name fixed for %s', (variant) => { expect(loadConfig(variant).name).toBe('All About Anything') @@ -96,7 +96,7 @@ describe('app.config variants', () => { }) it('keeps every variant installable alongside the others', () => { - let variants = ['aao', 'aao-dev', 'carls', 'carls-dev'] + let variants = ['aao', 'aao-dev', 'carls', 'carls-dev', 'aaa', 'aaa-dev'] let ids = variants.map((v) => loadConfig(v).ios?.bundleIdentifier) expect(new Set(ids).size).toBe(variants.length) }) @@ -175,11 +175,50 @@ describe('app.config link schemes', () => { }) describe('app.config default campus', () => { - test("every variant's default campus is registered", () => { + test("every variant's default campus is registered, or none, to ask", () => { // oxlint-disable-next-line typescript/no-require-imports let {isCampusId} = require('../source/campuses') as typeof import('../source/campuses') - for (let variant of ['aao', 'aao-dev', 'carls', 'carls-dev']) { - expect(isCampusId(loadConfig(variant).extra?.defaultCampus)).toBe(true) + for (let variant of ['aao', 'aao-dev', 'carls', 'carls-dev', 'aaa', 'aaa-dev']) { + let campus = loadConfig(variant).extra?.defaultCampus + expect(campus === null || isCampusId(campus)).toBe(true) } }) }) + +describe('app.config All About Anything', () => { + it.each([ + ['aaa', 'tech.frogpond.allaboutanything', 'All About Anything', 'allaboutanything'], + ['aaa-dev', 'tech.frogpond.allaboutanything.dev', 'AAA Dev', 'allaboutanythingDev'], + ])('builds %s as All About Anything', (variant, bundleIdentifier, displayName, scheme) => { + let config = loadConfig(variant) + expect(config.ios?.bundleIdentifier).toBe(bundleIdentifier) + expect(config.ios?.infoPlist?.CFBundleDisplayName).toBe(displayName) + expect(config.scheme).toBe(scheme) + expect(config.extra?.app).toBe('aaa') + expect(config.extra?.defaultCampus).toBeNull() + }) + + it('wears Windmill (Sky), and bundles the other icons as alternates', () => { + let config = loadConfig('aaa') + expect(config.ios?.icon).toBe('./assets/windmill-sky.icon') + expect(config.plugins).toContainEqual([ + './plugins/with-alternate-icons', + {primary: 'windmill-sky'}, + ]) + }) +}) + +describe('app.config Sentry', () => { + it.each([ + ['aao', 'all-about-olaf'], + ['carls', 'carls'], + ['aaa', 'all-about-anything'], + ])('sends %s to its own project, and hands the app its DSN', (variant, project) => { + let config = loadConfig(variant) + expect(config.plugins).toContainEqual([ + '@sentry/react-native/expo', + {organization: 'frog-pond-labs', project}, + ]) + expect(config.extra?.sentry?.dsn).toMatch(/^https:\/\/[0-9a-f]+@o524787\.ingest\./u) + }) +}) diff --git a/app.config.ts b/app.config.ts index 8fbc124652..71baf30735 100644 --- a/app.config.ts +++ b/app.config.ts @@ -23,9 +23,36 @@ const AAO_ICON = './assets/windmill.icon' /** The CARLS penguin, the CARLS app's own 1024px artwork. */ const CARLS_ICON = './assets/carls-penguin.xcassets/carls-penguin.appiconset/light.png' +/** All About Anything's identifier. */ +const AAA_BUNDLE_ID = 'tech.frogpond.allaboutanything' + +/** Windmill (Sky), an Icon Composer document, as All About Anything's primary. */ +const AAA_ICON = './assets/windmill-sky.icon' + /** - * Which app this build is, which `APP_VARIANT` must name: `aao`, `carls`, or - * their `-dev` builds, which install alongside the App Store's instead of + * Each app's Sentry project. A variant names its own organization too, so a + * future app can report somewhere else without code changes. A DSN only + * routes reports, so it isn't secret. + */ +const AAO_SENTRY = { + organization: 'frog-pond-labs', + project: 'all-about-olaf', + dsn: 'https://7f68e814c5c24c32a582f2ddc3d42b4c@o524787.ingest.sentry.io/5637838', +} +const CARLS_SENTRY = { + organization: 'frog-pond-labs', + project: 'carls', + dsn: 'https://a539fb427f9f4380971602c1e21e6959@o524787.ingest.us.sentry.io/5637839', +} +const AAA_SENTRY = { + organization: 'frog-pond-labs', + project: 'all-about-anything', + dsn: 'https://daa452d0505f2c883df86e0a0b6196fa@o524787.ingest.us.sentry.io/4512230081363968', +} + +/** + * Which app this build is, which `APP_VARIANT` must name: `aao`, `carls`, + * `aaa`, or their `-dev` builds, which install alongside the App Store's instead of * replacing it on your device. There is no default, so a forgotten variant * fails here rather than building and launching the other app. * @@ -33,6 +60,10 @@ const CARLS_ICON = './assets/carls-penguin.xcassets/carls-penguin.appiconset/lig * app (`extra.app`, read by source/lib/app-identity.ts), opening on Carleton, * with the penguin as its icon and the windmills to switch to. * + * `aaa` and `aaa-dev` build All About Anything: no default campus, so a fresh + * install asks, with Windmill (Sky) as its icon and its own scheme, which AAO + * and CARLS don't register. + * * The identity has to differ in three places, not one: iOS keys installs on the * bundle identifier, the home screen shows the name, and two apps claiming the * same URL scheme is undefined behaviour — whichever iOS feels like wins. @@ -53,6 +84,7 @@ const VARIANTS = { scheme: 'AllAboutOlaf', icon: AAO_ICON, primaryIcon: 'windmill', + sentry: AAO_SENTRY, }, 'aao-dev': { app: 'aao', @@ -62,6 +94,7 @@ const VARIANTS = { scheme: 'AllAboutOlafDev', icon: AAO_ICON, primaryIcon: 'windmill', + sentry: AAO_SENTRY, }, carls: { app: 'carls', @@ -72,6 +105,7 @@ const VARIANTS = { scheme: 'carls', icon: CARLS_ICON, primaryIcon: 'carls-penguin', + sentry: CARLS_SENTRY, }, 'carls-dev': { app: 'carls', @@ -81,6 +115,27 @@ const VARIANTS = { scheme: 'carlsDev', icon: CARLS_ICON, primaryIcon: 'carls-penguin', + sentry: CARLS_SENTRY, + }, + aaa: { + app: 'aaa', + defaultCampus: null, + displayName: 'All About Anything', + bundleIdentifier: AAA_BUNDLE_ID, + scheme: 'allaboutanything', + icon: AAA_ICON, + primaryIcon: 'windmill-sky', + sentry: AAA_SENTRY, + }, + 'aaa-dev': { + app: 'aaa', + defaultCampus: null, + displayName: 'AAA Dev', + bundleIdentifier: `${AAA_BUNDLE_ID}.dev`, + scheme: 'allaboutanythingDev', + icon: AAA_ICON, + primaryIcon: 'windmill-sky', + sentry: AAA_SENTRY, }, } as const @@ -320,6 +375,7 @@ const config: ExpoConfig = { app: variant.app, defaultCampus: variant.defaultCampus, primaryIcon: variant.primaryIcon, + sentry: {dsn: variant.sentry.dsn}, }, plugins: [ @@ -386,10 +442,7 @@ const config: ExpoConfig = { ['./plugins/with-app-variant', {variant: requested}], [ '@sentry/react-native/expo', - { - organization: 'frog-pond-labs', - project: variant.app === 'carls' ? 'carls' : 'all-about-olaf', - }, + {organization: variant.sentry.organization, project: variant.sentry.project}, ], // react-native-enriched-markdown 1.0.2 dropped its Expo config plugin; // its options now live in the `enriched-markdown` block of package.json. diff --git a/ios_scripts/ci_post_clone.sh b/ios_scripts/ci_post_clone.sh index 751cd66005..8d4f916b2a 100755 --- a/ios_scripts/ci_post_clone.sh +++ b/ios_scripts/ci_post_clone.sh @@ -5,17 +5,10 @@ echo "Running ci_post_clone.sh" export MISE_RUBY_COMPILE='false' export MISE_AUTO_INSTALL='false' -export SENTRY_ORG='frog-pond-labs' # Each Xcode Cloud workflow names its app in APP_VARIANT, which app.config.ts -# requires: aao or carls. -case "${APP_VARIANT:-}" in - aao) export SENTRY_PROJECT='all-about-olaf' ;; - carls) export SENTRY_PROJECT='carls' ;; - *) - echo "error: set APP_VARIANT to aao or carls in this Xcode Cloud workflow's environment" >&2 - exit 1 - ;; -esac +# requires: aao, carls or aaa. Prebuild writes that variant's Sentry org and +# project to ios/sentry.properties, which the Sentry build phases read. +: "${APP_VARIANT:?set APP_VARIANT to aao, carls or aaa in the Xcode Cloud workflow environment}" # Xcode Cloud runs this with ci_scripts as the working directory, and it must # live beside the .xcworkspace, so the repository root is two levels up. diff --git a/ios_scripts/ci_post_xcodebuild.sh b/ios_scripts/ci_post_xcodebuild.sh index 7787697573..53ed9f8ed1 100755 --- a/ios_scripts/ci_post_xcodebuild.sh +++ b/ios_scripts/ci_post_xcodebuild.sh @@ -25,8 +25,8 @@ cd ../../ brew_prefix="$(brew --prefix)" export PATH="${brew_prefix}/bin:$PATH" -export SENTRY_ORG='frog-pond-labs' -export SENTRY_PROJECT='all-about-olaf' +# The variant's Sentry org and project, as prebuild wrote them from app.config.ts. +export SENTRY_PROPERTIES="${PWD}/ios/sentry.properties" # Run one sentry-cli step, and turn a failure into a warning, so a Sentry outage # cannot hold up a TestFlight build. diff --git a/mise.toml b/mise.toml index 0886cb0b6d..5d197fca32 100644 --- a/mise.toml +++ b/mise.toml @@ -112,6 +112,20 @@ eval "set -- $usage_args" expo run:ios ${usage_device:+--device "$usage_device"} "$@" ''' +[tasks."aaa:ios"] +description = "Build and run All About Anything (AAA Dev) on a simulator or device" +usage = ''' + arg "[device]" help="The device to run on" default="" env="DEVICE" + complete "device" run=#"{{ vars.device_names }}"# + arg "[args]..." var=#true double_dash="required" help="Passed to expo run:ios" +''' +env = { APP_VARIANT = "aaa-dev" } +depends = [{ task = "prebuild", env.APP_VARIANT = "aaa-dev" }] +run = ''' +eval "set -- $usage_args" +expo run:ios ${usage_device:+--device "$usage_device"} "$@" +''' + [tasks.start] description = "Start Metro bundler" run = "expo start" @@ -179,7 +193,7 @@ bundle install --quiet # ios/ holds one app at a time. Prebuilding over another variant's project # keeps that one's icons and settings, so a change of variant starts afresh. -variant="${APP_VARIANT:?set APP_VARIANT to aao, aao-dev, carls or carls-dev}" +variant="${APP_VARIANT:?set APP_VARIANT to aao, aao-dev, carls, carls-dev, aaa or aaa-dev}" clean="" if test "$(cat ios/.app-variant 2>/dev/null)" != "$variant"; then clean="--clean" diff --git a/source/init/constants.ts b/source/init/constants.ts index f8173c5977..0e715fcfaa 100644 --- a/source/init/constants.ts +++ b/source/init/constants.ts @@ -1,13 +1,11 @@ import Constants from 'expo-constants' import {setVersionInfo, setTimezone} from '@frogpond/constants' -import {APP} from '../lib/app-identity' - -/** Each app reports to its own Sentry project: all-about-olaf, or carls. */ -export const SENTRY_DSN = - APP === 'carls' - ? 'https://a539fb427f9f4380971602c1e21e6959@o524787.ingest.us.sentry.io/5637839' - : 'https://7f68e814c5c24c32a582f2ddc3d42b4c@o524787.ingest.sentry.io/5637838' +/** The build's Sentry DSN, which app.config.ts names per variant; undefined reports nothing. */ +export const SENTRY_DSN: string | undefined = + typeof Constants.expoConfig?.extra?.sentry?.dsn === 'string' + ? Constants.expoConfig.extra.sentry.dsn + : undefined /** * The full semver, prerelease tag and all, taken from the app config rather diff --git a/source/lib/app-identity.ts b/source/lib/app-identity.ts index 7ebdf80e63..6bfe35afaa 100644 --- a/source/lib/app-identity.ts +++ b/source/lib/app-identity.ts @@ -1,13 +1,20 @@ import Constants from 'expo-constants' -/** Which app a build is: All About Olaf, or CARLS. */ -export type AppIdentity = 'aao' | 'carls' +/** Which app a build is: All About Olaf, CARLS, or All About Anything. */ +export type AppIdentity = 'aao' | 'carls' | 'aaa' + +/** Whether `value` names an app. */ +function isAppIdentity(value: unknown): value is AppIdentity { + return value === 'aao' || value === 'carls' || value === 'aaa' +} /** * The app this build is, as app.config.ts's `APP_VARIANT` chose it. Which - * campus it opens on is DEFAULT_CAMPUS; dev mode switches campus in either. + * campus it opens on is DEFAULT_CAMPUS; dev mode switches campus in any. */ -export const APP: AppIdentity = Constants.expoConfig?.extra?.app === 'carls' ? 'carls' : 'aao' +export const APP: AppIdentity = isAppIdentity(Constants.expoConfig?.extra?.app) + ? Constants.expoConfig.extra.app + : 'aao' /** * The campus a fresh install opens on, as app.config.ts's variant names it, or