Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/run-on-simulator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 45 additions & 6 deletions __tests__/app.config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
})

Expand All @@ -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')
Expand All @@ -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)
})
Expand Down Expand Up @@ -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)
})
})
65 changes: 59 additions & 6 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,47 @@ 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.
*
* `carls` and `carls-dev` build the same code as CARLS: Carleton's
* 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.
Expand All @@ -53,6 +84,7 @@ const VARIANTS = {
scheme: 'AllAboutOlaf',
icon: AAO_ICON,
primaryIcon: 'windmill',
sentry: AAO_SENTRY,
},
'aao-dev': {
app: 'aao',
Expand All @@ -62,6 +94,7 @@ const VARIANTS = {
scheme: 'AllAboutOlafDev',
icon: AAO_ICON,
primaryIcon: 'windmill',
sentry: AAO_SENTRY,
},
carls: {
app: 'carls',
Expand All @@ -72,6 +105,7 @@ const VARIANTS = {
scheme: 'carls',
icon: CARLS_ICON,
primaryIcon: 'carls-penguin',
sentry: CARLS_SENTRY,
},
'carls-dev': {
app: 'carls',
Expand All @@ -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

Expand Down Expand Up @@ -320,6 +375,7 @@ const config: ExpoConfig = {
app: variant.app,
defaultCampus: variant.defaultCampus,
primaryIcon: variant.primaryIcon,
sentry: {dsn: variant.sentry.dsn},
},

plugins: [
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 3 additions & 10 deletions ios_scripts/ci_post_clone.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions ios_scripts/ci_post_xcodebuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 5 additions & 7 deletions source/init/constants.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 11 additions & 4 deletions source/lib/app-identity.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading