app-variants-and-environment
Wires APP_VARIANT builds: per-variant ids, names, icons, schemes, EXPO_PUBLIC_* as public, secrets in EAS/CI, test code out of store bundles.
Skill by zakariaf
Chalk Labs
Need help deploying app-variants-and-environment?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/zakariaf-app-variants-and-environment
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: testing, devops, security
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Node.js package
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/app-variants-and-environment
What's inside
- Skill name
app-variants-and-environment- Description
- Wires APP_VARIANT builds: per-variant ids, names, icons, schemes, EXPO_PUBLIC_* as public, secrets in EAS/CI, test code out of store bundles. Use when adding a variant, env var, .env or secret. Not for signing (eas-build-and-submit).
- License
- MIT
- Instructions
- 121 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
Agent Skills spec issues
- files: referenced but missing: scripts/metro/metro-kits.ts
Files (54)
- SKILL.md
- shared.json · assets/
- template-placeholders.json · assets/
- environment.ts · examples/
- variant-proof.md · examples/
- environment-variables.md · references/
- pitfalls-and-tests.md · references/
- secrets.md · references/
- stripping-test-code.md · references/
- variant-matrix.md · references/
- check-env.mjs · scripts/
- check-lib.mjs · scripts/
- check-variant-bundle.mjs · scripts/
- check-variants.mjs · scripts/
- env-scan.mjs · scripts/lib/
- source-scan.mjs · scripts/lib/
- make-variant-icons.mjs · scripts/
- selftest.mjs · scripts/
- app.config.ts · templates/
- dot-env.example · templates/
- eas.json · templates/
- metro.config.js · templates/
- compose-metro-config.test.ts · templates/scripts/metro/
- compose-metro-config.ts · templates/scripts/metro/
- public-env-cache-version.test.ts · templates/scripts/metro/
- public-env-cache-version.ts · templates/scripts/metro/
- app-variant.test.ts · templates/src/config/
- app-variant.ts · templates/src/config/
- current-app-variant.test.ts · templates/src/config/
- current-app-variant.ts · templates/src/config/
- env.d.ts · templates/src/
- dot-gitignore · tests/bases/app/
- package.json · tests/bases/app/
- _layout.tsx · tests/bases/app/src/app/
- environment.ts · tests/bases/app/src/config/
- empty-state.tsx · tests/bases/app/src/features/tasks/
- use-tasks-model.ts · tests/bases/app/src/features/tasks/
- test-only-entry.ts · tests/bases/app/src/
- test-only.ts · tests/bases/app/src/
- leak.ts · tests/bases/decoys/.agents/skills/decoy/src/
- leak.ts · tests/bases/decoys/skills/decoy/src/
- leak.ts · tests/bases/decoys/src/vendor/kit/
- CASES.json · tests/fixtures/check-env/
- SUITE.json · tests/fixtures/check-env/
- EXPECT.txt · tests/fixtures/check-env/good/
- CASES.json · tests/fixtures/check-variant-bundle/
- SUITE.json · tests/fixtures/check-variant-bundle/
- EXPECT.txt · tests/fixtures/check-variant-bundle/good/
- CASES.json · tests/fixtures/check-variants/
- SUITE.json · tests/fixtures/check-variants/
- EXPECT.txt · tests/fixtures/check-variants/good/
- CASES.json · tests/fixtures/make-variant-icons/
- SUITE.json · tests/fixtures/make-variant-icons/
- EXPECT.txt · tests/fixtures/make-variant-icons/good/
SKILL.md (first lines)
# App variants and environment
One Expo app builds as three variants, `development` (the dev client), `preview` (internal testing) and `production` (the stores), chosen by `APP_VARIANT`. Each variant installs beside the others and says what it is. Every `EXPO_PUBLIC_*` value is treated as public, secrets stay in EAS or CI, and test-only code never reaches a store bundle. Three checkers prove it: the config per variant, the source and files, and the exported bundle.
## Rules that must hold
1. **One resolver decides the variant.** `app.config.ts` takes it from `resolveAppVariant(process.env)` in `src/config/app-variant.ts`: only `development`, `preview` or `production`; an unknown name throws; `APP_VARIANT` and `EXPO_PUBLIC_APP_VARIANT` must be equal; both unset means development for local work and throws in a production bundle (`NODE_ENV=production`: `expo export`, `eas update`, `expo start --no-dev`) or on EAS Build (`EAS_BUILD=true`). The variant comes from the command, the eas.json profile or the EAS environment, never from a committed env file. Why: the config and the JS bundle are evaluated in different processes, so a typo or a lost variable otherwise ships the wrong app without an error, and Expo CLI loads a committed `.env` into every local `start`, `export` and `prebuild`.
2. **Every variant installs beside the others.** Production keeps the store id; development and preview append `.dev` and `.preview` to the iOS bundle identifier and the Android package, and every id is valid on both platforms. Why: a shared id overwrites the store app and its data on a tester's phone. Services keyed by app id (push, Sign in with Apple, IAP products, universal links, Firebase files) need each id registered or are tested on production-id builds, and ids built on the app id (App Groups, iCloud and keychain groups, extension bundle ids) derive from the variant's id.
3. **Every variant shows what it is.** Its own name (`Tasks (Dev)`, `Tasks (Preview)`, the store name for production), its own icon files on iOS and Android (which exist: production keeps the store icons in `assets/icons/production/`, development and preview get badged copies), its own URL scheme, and the dev client's generated `exp+<slug>` scheme only in development. Why: testers, screenshots and bug reports must name the build; a missing icon file stops prebuild and every EAS Build with ENOENT; with two installed apps on one scheme iOS opens either one (which is undefined) while Android asks the user to choose.
4. **Every EAS build profile names its variant twice and its environment once.** `env.APP_VARIANT` = `env.EXPO_PUBLIC_APP_VARIANT` = `environment` = the variant; only development sets `developmentClient`; extra profiles extend a variant profile; a shared `base` profile the variants extend names no variant, environment or `developmentClient` a child contradicts. The same two variables also exist as plain-text EAS environment variables in each environment, with equal values (EAS CLI warns that the profile's value wins). Why: eas.json `env` reaches builds only; `eas update --environment <name>` reads the EAS environment and nothing else.
5. **`EXPO_PUBLIC_*` is public.** Metro inlines each value into the JS bundle, where anyone with the app can read it, so no `EXPO_PUBLIC_` name or value is a secret: `SECRET`, `TOKEN`, `PASSWORD`, private keys, payment secret keys, and paid or LLM API keys (OpenAI, Anthropic, Gemini, ElevenLabs, Twilio, ...). The app calls a server that holds the key, such as an Expo Router API route. A key that is public by design (a Maps key restricted to the app id, a Stripe publishable key) goes into `publicEnvAllow` with a reason. Why: nothing that ships in the binary can be hidden, minified or bytecode alike, and a leaked LLM key bills the developer's account.
6. **App code reads the environment in one place, the way Metro inlines it.** Only `src/config/` reads `process.env` (except the literal comparison at an inclusion `require()`, rule 9), only as `process.env.EXPO_PUBLIC_NAME` with dot access; the variant comes from `currentAppVariant()`, never from `Constants.expoConfig.extra` or `__DEV__`. Server code is not app code: API routes (`src/app/**/*+api.ts`), server middleware and the helper folders named in `serverDirs` read their secrets from `process.env` at request time, and app code never imports them. Why: brackets, destructuring and non-public names are `undefined` in a release build while Jest still sees them, and a second source of truth drifts.
7. **Secrets live in EAS or CI, never in the repo or the app.** Build-time secrets (`SENTRY_AUTH_TOKEN`, store keys) are EAS variables with secret visibility or CI secrets; runtime secrets for API routes are EAS variables with sensitive visibility (EAS Hosting cannot deploy secret ones); eas.json `env` and committed env files hold none; no secret flows into the app config, whose public part (`extra`, plugin options) is embedded in every build. Why: committed and embedded values are readable for good; rotating the secret is the only cure.
8. **Secret files are git-ignored.** `.gitignore` covers `.env*.local`, `*.p8`, `*.p12`, `*.jks`, `*.keystore`, `*.mobileprovision`, `credentials.json` when a profile uses local credentials, and every key file eas.json points at. Why: one `git add .` publishes a signing key, and keys cannot be unpublished.
9. **Code is left out of a bundle only by a literal comparison at the `require()`** (this skill owns that rule; `debug-menu-and-test-hooks` owns the gate file, `architecture-and-boundaries` the import side). Test-only code sits in one ternary with `process.env.EXPO_PUBLIC_APP_VARIANT === 'production'`, never behind an imported constant, a helper call, `__DEV__`, `NODE_ENV` or an `if` block; any other inclusion flag works the same way (`process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true' ? require(...) : null`). Debug-bundle tooling (Reactotron, a devtools package) is not test-only code: its `__DEV__` gate is right, and other such modules go into `devOnlyRequires`. Why: verified on Expo SDK 57, Metro folds only literals in the same expression, so an imported `IS_TEST_BUILD` or `STORYBOOK_ENABLED` kept its module in the production bundle; `__DEV__` folds but also strips preview builds.
10. **Metro's cache is keyed on the public values.** `metro.config.js` sets `cacheVersion` from every `EXPO_PUBLIC_*` name and value, so it changes with any public value and differs per variant. Why: verified on Expo SDK 57, a production export made after a development export reused the development bundle byte for byte, test-only code included. It bites every `expo export` and every release build on a machine with `CI` set; only `eas update --environment` clears the cache by itself.
11. **The exported bundle proves the strip.** After a change to variants, gates, Metro or variables, export development and production back to back without `--clear` and check both: production holds no test-only sentinel and no secret; development holds every sentinel. Why: the gate, the cache key and the variables only work together, and only the artifact shows it.
## Workflow
1. **Decide the identities.** Read [references/variant-matrix.md](references/variant-matrix.md): the three variants, ids, names, icons, schemes, which services need each id registered, and the moments a variant enters the binary on iOS and Android.
2. **Merge the templates into the app** (line 1 names each repo path; fill `__APP_ID__`, `__APP_NAME__`, `__APP_SLUG__`, `__APP_SCHEME__`). Nothing the app already configures is lost:
- First save the production config: `mkdir -p dist && APP_VARIANT=production EXPO_PUBLIC_APP_VARIANT=production npx expo config --type public --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.stringify(JSON.parse(s),null,2)))' > dist/config-before.json` (`dist/` is git-ignored).
- Copy `templates/src/config/app-variant.ts` and its test (the canonical resolver, shared byte for byte with the build, release and ads skills), `templates/src/config/current-app-variant.ts` and its test, and `templates/src/env.d.ts`.
- Build `app.config.ts` from `templates/app.config.ts` and move every value of the old `app.json` or app config into its returned object: `owner`, `version`, `updates.url`, `runtimeVersion`, `extra.eas.projectId` (the EAS project link), every plugin with its options, `ios.infoPlist` usage strings, `android.permissions`, colours, splash. Only the id, name, scheme, icons and dev-client scheme depend on the variant; an old `extra.appVariant` goes (the app asks `currentAppVariant()`).
- Move the app's icon files into `assets/icons/production/`: `icon.png`, its Icon Composer folder (`expo.icon` in the SDK 57 template; set `STORE_IOS_ICON` to its name, or to `'icon.png'` when there is none), and the adaptive layers renamed `android-foreground.png`, `android-background.png`, `android-monochrome.png`. Then `node ${CLAUDE_SKILL_DIR}/scripts/make-variant-icons.mjs .` writes badged copies to `assets/icons/development/` and `assets/icons/preview/` (amber band, violet hatched band), so every variant builds; `icons-app-icon-and-splash` may redraw them at the same paths.
- Metro: copy `templates/metro.config.js` with `templates/scripts/metro/public-env-cache-version.ts` and `compose-metro-config.ts` (each with its test). Its Metro slot composes the optional kits of `scripts/metro/metro-kits.ts` (crash reporting's Sentry config is one; app-startup-and-bootstrap's slot contract), so an old file's wrappers (Sentry, NativeWind, SVG) move into a `MetroKit` there (`baseConfig` for one that builds on Expo's default, else `transform`). An app that keeps its own `metro.config.js` adds `config.cacheVersion = publicEnvCacheVersion(process.env)` on the object it finally exports.
- `templates/eas.json`: merge its three `build` profiles into the app's eas.json (a shared `base` they extend may stay). When there is no eas.json yet, leave it: `eas-build-and-submit` creates it with these profiles, and until then `check-variants` prints the expected eas.json SKIP.
- `templates/dot-env.example` to `.env.example`, and add `!.env.example` to `.gitignore` when `.env*` is ignored.
- `npx expo install expo-dev-client expo-splash-screen` (the config names both plugins; the development profile builds the dev client).
- Save `dist/config-after.json` the same way and `diff dist/config-before.json dist/config-after.json`: only icon paths, the `expo-dev-client` plugin and an old `extra.appVariant` may change. Then delete `app.json`.
3. **Route every read through `src/config/`.** Replace variant checks with `currentAppVariant()`; give each other public value a typed reader like [examples/environment.ts](examples/environment.ts) and a line in `src/env.d.ts`. Read [references/environment-variables.md](references/environment-variables.md) for inlining, `.env` loading, EAS environments and CI.
4. **Place every secret.** Read [references/secrets.md](references/secrets.md), list the build-time and runtime secrets, move any paid or LLM API call behind an API route, and hand the developer the `eas env:set` commands (secret visibility for build-time secrets, sensitive for API-route secrets, plain text for `APP_VARIANT` and `EXPO_PUBLIC_APP_VARIANT` in all three environments). They run in the developer's Expo account: stop and ask before creating, changing or deleting any EAS variable, and before rotating a leaked key.
5. **Gate test-only code** with the literal comparison and export a sentinel from the test-only entry: [references/stripping-test-code.md](references/stripping-test-code.md). The gate file and the debug registry belong to `debug-menu-and-test-hooks`.
6. **Run the app's gates:** `npm run typecheck`, `npx eslint . --max-warnings 0`, `npx jest src/config scripts/metro --ci --maxWorkers=2`.
7. **Look at each variant:** `APP_VARIANT=preview EXPO_PUBLIC_APP_VARIANT=preview npx expo config --type public` (and development, production), then `APP_VARIANT=preview EXPO_PUBLIC_APP_VARIANT=preview npx expo prebuild --no-install --clean` proves the icons and plugins resolve. Before a local build after switching variants, run that prebuild with the build's two variables: `ios/` and `android/` keep the previous variant until regenerated.
8. **Check:** `node ${CLAUDE_SKILL_DIR}/scripts/check-variants.mjs .` and `node ${CLAUDE_SKILL_DIR}/scripts/check-env.mjs .`. Each `FAIL` line names the file, the rule and the fix; [references/pitfalls-and-tests.md](references/pitfalls-and-tests.md) explains every rule. Fix and rerun until both print `RESULT: PASS`.
9. **Prove the strip** when variants, gates, `metro.config.js` or variables changed: export both variants back to back into `dist/variant-proof/development` and `dist/variant-proof/production` (commands in [examples/variant-proof.md](examples/variant-proof.md); `dist/` is already git-ignored), then `node ${CLAUDE_SKILL_DIR}/scripts/check-variant-bundle.mjs . --bundle dist/variant-proof/development --variant development` and the same with `dist/variant-proof/production --variant production`. Delete `dist/variant-proof/` afterwards.
10. **Report** the three ids and names, the EAS variables the developer still has to create, the SKIP lines, and every `RESULT` line.
## Definition of done
- [ ] `src/config/app-variant.ts` is the canonical file, `app.config.ts` resolves the variant through it, the production config diff shows only the intended keys, and there is no `app.json`.
- [ ] `npx expo config --type public` shows three distinct ids, names, icons and schemes; an unknown or mismatched variant fails; every icon file exists and a preview prebuild succeeds.
- [ ] When eas.json exists, it has development, preview and production profiles with `APP_VARIANT`, `EXPO_PUBLIC_APP_VARIANT` and `environment` equal; the developer has the `eas env:set` list for the EAS environments.
- [ ] App code reads the variant only through `currentAppVariant()` and other public values only through `src/config/` (an inclusion flag's literal comparison at its `require()` excepted); no secret (an LLM or paid API key included) has an `EXPO_PUBLIC_` name or sits in a committed file; every plugin the config names is installed.
- [ ] `metro.config.js` keys `cacheVersion` on the `EXPO_PUBLIC_*` values; the app's typecheck, lint and Jest pass.
- [ ] When strip-relevant code changed: `node ${CLAUDE_SKILL_DIR}/scripts/check-variant-bundle.mjs . --bundle dist/variant-proof/production --variant production` and the development run print `RESULT: PASS`.
- [ ] Expected SKIP lines only: `SKIP eas.json [eas-profile] not yet created (eas-build-and-submit creates it)` before EAS is set up, and `SKIP src/test-only.ts [bundle-sentinel] not yet created (debug-menu-and-test-hooks creates it)` while the app has no test-only gate. A `note:` line about unpinned submit ids is a hand-off to `eas-build-and-submit`, not a failure.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-env.mjs .` prints `RESULT: PASS`
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-variants.mjs .` prints `RESULT: PASS`
## Anti-patterns
- **Copying `templates/app.config.ts` over the app's config.** The EAS project link, `updates.url`, `runtimeVersion`, plugin options, usage strings and permissions vanish without an error; merge, then diff the production config.
- **`APP_VARIANT` only in eas.json.** `eas update --environment production` never reads eas.json: with this resolver the update fails, and with one that defaults it ships as development, test hooks included. Create the two variables in every EAS environment as well.
- **`eas update` without `--environment` in CI.** EAS CLI asks for an environment only outside CI, and even then the answer only labels the update: the export gets no EAS variables, loads the runner's `.env` files and reuses a warm Metro cache. Always pass `--environment <variant>`.
- **`const isDev = process.env.APP_VARIANT === 'development'` in app code.** `APP_VARIANT` is not inlined: it is `undefined` in every release build. Call `currentAppVariant()`.
- **`__DEV__` as "the development variant".** `__DEV__` says whether Metro served a debug bundle; preview and production are release builds, and a development build can load a release bundle. Ask the variant.
- **`.env.staging` or `NODE_ENV=staging` to pick an environment.** `npx expo export` forces `NODE_ENV=production`; variants and EAS environments are the switch.
- **A secret behind `EXPO_PUBLIC_` "because the bundle is minified".** Hermes bytecode keeps string literals readable. Call a server, or keep the secret in a build step.
- **`EXPO_PUBLIC_OPENAI_API_KEY` so the app can call the model directly.** Anyone can lift the key from the bundle and spend the developer's quota. Put the call in an API route (`src/app/api/<name>+api.ts`) that reads `OPENAI_API_KEY` from `process.env`, deployed with `eas deploy --environment production`.
- **A standalone `eas submit` with nothing exported.** It evaluates the config without the build profile env, so the resolver gives the development id. Export the production pair for it, or pin the submit ids (`eas-build-and-submit`).
- **One bundle id for every variant to keep push or IAP simple.** Every install replaces the store app. Register the extra ids, or test those services on production-id TestFlight or Play internal builds.
- **`IS_TEST_BUILD ? require('./debug-menu') : null`, or `STORYBOOK_ENABLED ? require(...)` with the flag read in `src/config/`.** Metro cannot fold an imported constant: the module ships (both verified on SDK 57). Write the literal comparison at the call.
- **`APP_VARIANT=production` in a committed `.env`.** Every developer's `expo start` and `prebuild` then builds the store identity; the variant belongs on the command line, in eas.json and in the EAS environments.
- **`npx expo export --clear` to fix a stale variant.** It hides the missing cache key until the next build; key `cacheVersion` on the public values.
- **Passing a token into `extra` or plugin options to use it later.** The public config is embedded in the app; tools read secrets from their own environment.
- **Exporting the variables for `expo prebuild` only, then building in a fresh shell.** The Constants step and Metro run again during the native build and need the same two variables.
## Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| [references/variant-matrix.md](references/variant-matrix.md) | The three variants, the resolver contract per command, ids, names, icons, schemes, services and entitlements keyed by id, EAS commands outside a build, the moments a variant enters the binary | Workflow step 1 |
| [references/environment-variables.md](references/environment-variables.md) | How `EXPO_PUBLIC_*` inlining works (verified on SDK 57), `.env` loading, the readers in `src/config/`, server code (API routes), EAS environments, CI | Workflow step 3 |
| [references/secrets.md](references/secrets.md) | Public versus secret (LLM and paid API keys), where each secret lives (API routes for runtime secrets), what ships in the binary, the canary check, `.gitignore`, leak response | Workflow step 4 |
| [references/stripping-test-code.md](references/stripping-test-code.md) | The inclusion gate, the Metro cache trap, the SDK 57 experiments, sentinels, the bundle proof | Workflow steps 5 and 9 |
| [references/pitfalls-and-tests.md](references/pitfalls-and-tests.md) | Symptoms with causes and fixes, test recipes, every checker rule, `rn-skills.config.json` keys | A check fails; writing tests |
| `templates/src/config/app-variant.ts` | The canonical resolver: `resolveAppVariant`, `parseAppVariant`, `variantIdentity`, `variantEnv` (synced from the library; do not edit here) | Workflow step 2 |
| `templates/src/config/app-variant.test.ts` | Its tests, including a fast-check property (synced; do not edit here) | With the resolver |
| `templates/src/config/current-app-variant.ts` | `currentAppVariant()`: the one runtime reader of the variant | Workflow step 2 |
| `templates/src/config/current-app-variant.test.ts` | Its tests | With the reader |
| `templates/src/env.d.ts` | The typed `EXPO_PUBLIC_*` declarations both TypeScript programs read (synced from the library; do not edit here) | Workflow step 2; each new public variable |
| `templates/app.config.ts` | The app config: identity, icons (`iconsFor`, `STORE_IOS_ICON`), scheme and dev-client scheme per variant; comments show where merged values go | Workflow step 2 |
| `templates/metro.config.js` | Metro config with `cacheVersion` keyed on the public values | Workflow step 2 |
| `templates/scripts/metro/public-env-cache-version.ts` | The tested cache-key helper `metro.config.js` requires | Workflow step 2 |
| `templates/scripts/metro/compose-metro-config.ts` | The Metro slot: `readMetroKits` (the optional `scripts/metro/metro-kits.ts`) and `composeMetroConfig` (Expo's default or one kit's base, then each kit's transform); `metro.config.js` requires it | Workflow step 2; a kit that needs Metro |
| `templates/scripts/metro/compose-metro-config.test.ts` | Its tests: no kits, one base and transforms in order, two bases refused, the kits file | With the helper |
| `templates/scripts/metro/public-env-cache-version.test.ts` | Its tests | With the helper |
| `templates/eas.json` | The three build profiles wired to their variant and EAS environment, merged into the app's eas.json (`eas-build-and-submit` creates the file) | Workflow step 2 |
| `templates/dot-env.example` | `.env.example`: every variable by name, no values | Workflow step 2 |
| [examples/environment.ts](examples/environment.ts) | A typed public-environment reader for the Tasks app (shape only) | Workflow step 3 |
| [examples/variant-proof.md](examples/variant-proof.md) | The back-to-back export proof with real output | Workflow step 9 |
| `scripts/make-variant-icons.mjs` | Writes badged development and preview icons from `assets/icons/production/` (checks the sources are square PNGs, and that the two themed icons differ in alpha) | Workflow step 2 |
| `scripts/check-variants.mjs` | Evaluates the config per variant and per broken environment (committed env files loaded): resolve, plugins installed, ids, names, icon files and icons, schemes, eas.json profiles (shared bases too), secrets in the config; loads metro.config.js to prove the cache key | Workflow step 8 |
| `scripts/check-env.mjs` | Scans app code (`src/`, a root `app/`, the entry and everything they import) and files: public-env secrets (LLM and paid API keys included, the app config too), the one reader (server code exempt), the variant reader, inclusion decided at the `require()`, server imports, secrets and variants in eas.json and env files, `.gitignore` | Workflow step 8 |
| `scripts/check-variant-bundle.mjs` | Checks an exported bundle for sentinels and secrets against its variant (secrets even before a test-only gate exists) | Workflow step 9 |
| `scripts/lib/env-scan.mjs` | Shared helpers: secret names and values, dotenv, `.gitignore` matching, eas.json profiles | Only when changing a checker |
| `scripts/lib/source-scan.mjs` | Dependency-free source lexer (synced from the library; do not edit here) | Only when changing a checker |
| `scripts/check-lib.mjs` | Shared checker library (synced from the library; do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Runs every checker against the fixtures | After changing a checker or a template |
| `tests/bases/` | The base app and the planted decoys the fixtures assemble from | When adding a fixture |
| `tests/fixtures/` | Overlay cases per script: good, bad cases per rule, pass and error cases | When adding a rule |
| `assets/shared.json` | The shared files this skill syncs in | When adding a shared file |
| `assets/template-placeholders.json` | Values for the `__UPPER_SNAKE__` placeholders in template checks | When a template gains a placeholder |
## Related skills
- `expo-prebuild-and-config-plugins` - app.config.ts as the single source, config plugins, never editing native folders.
- `eas-build-and-submit` - the rest of eas.json: credentials, versions, submit profiles; Expo's official `eas-app-stores` skill is the EAS API reference.
- `debug-menu-and-test-hooks` - the test-only gate file, the debug registry and the sentinel this skill's bundle check reads.
- `icons-app-icon-and-splash` - may redraw the badged development and preview icons at `assets/icons/<variant>/` (icon.png 1024 x 1024, android-foreground/background/monochrome.png).
- `privacy-and-store-declarations` - the SDK inventory, store privacy answers and repository-wide secret audit.
- `networking-and-server-state` - the `ApiClient` that calls the app's API routes; Expo's official `eas-hosting` skill is the API-route and EAS Hosting reference.
- `crash-reporting-and-observability` - Sentry release names per variant and the upload token as an EAS secret.
- `ota-updates` - channels per variant and `eas update --environment`.
- `ci-pipeline-and-gates` - CI secrets and the variables every CI job exports.
- `ios-simulator-build`, `android-emulator-build` - building and proving each variant on a named device.From zakariaf/ReactNative-Skills, MIT.
More from zakariaf/ReactNative-Skills
- 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
android-release-play
zakariaf
Ships Android App Bundles to Google Play: upload key, Play App Signing, AAB gate, versionCode, tracks, rollout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
architecture-and-boundaries
zakariaf
Places app code in the src/ layout and checks import boundaries: one-way zones, pure core, ports with fakes, one composition root.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
async-safety
zakariaf
Prevents silent async bugs: effect races, stale results, lost promises, leaked listeners and timers, AbortSignal gaps.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
audio-and-haptics-feedback
zakariaf
Builds SFX and haptics behind ports.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
auth-and-secure-storage
zakariaf
Builds sign-in - tokens in expo-secure-store, shared refresh, protected routes, OAuth PKCE, Apple sign-in.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
background-tasks
zakariaf
Builds expo-background-task jobs: idempotent, budgeted runs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9
Signals
- Stars
- 11
- Forks
- 2
- Open issues
- 0
- Last commit (this folder)
- Oct 17, 2026 (13 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/zakariaf/ReactNative-Skills
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from zakariaf
- 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get app-variants-and-environment running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
app-variants-and-environment skill. Agent Mart. https://agentmart.dev/skills/zakariaf-app-variants-and-environment (last updated 2026-10-24)