chore(qa): QA/staging environment seam (web+api+android internal) (#211) - #239
Conversation
) Internal-track Android builds silently baked in the prod API base because android-release.yml never injected EXPO_PUBLIC_API_BASE at prebuild. Derive the build target from the release track (internal => QA, all other tracks => prod): inject EXPO_PUBLIC_API_BASE from vars.QA_API_BASE, force AdMob test IDs, and gate the prod-AdMob validation steps off for the QA build. Add a first-class eas qa build profile + submit.qa internal-track mapping, and a QA switch in the android-generate skill. QA web + API run on operator-provisioned infra (Render QA service + env group with ASPNETCORE_ENVIRONMENT=Production, QA Supabase, QA web deploy, and vars.QA_API_BASE); those are documented in the PR, not code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Solid, focused fix for the silent-prod-bake bug on the internal track. The Resolve build target step is clean: it derives prod vs QA from the existing track input (no new workflow param), fails loudly when vars.QA_API_BASE is unset so there's no silent fallback, and writes resolved env vars cleanly through GITHUB_ENV. Gating the prod-AdMob validation steps off for internal builds is the correct call. The eas.json qa profiles mirror existing profile shapes exactly. Nothing to change here.
Combine #233's job-level AdMob env hoist with #239's QA build-target resolution: prebuild AND bundleRelease map the BUILD_* overrides, so the internal/QA track ships test IDs at bundle time (not just prebuild); gate #233's shipped-AAB real-ID guard to non-internal tracks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Code Review: PR #239
Scope: PR #239 — chore(qa): QA/staging environment seam (web+api+android internal) (#211)
Recommendation: NEEDS WORK
Summary
This PR stands up a QA build target for the Android internal-track workflow by injecting EXPO_PUBLIC_API_BASE at prebuild time and adding a qa profile to eas.json. The core intent is sound and the fail-loud guard on an unset QA_API_BASE is good defensive engineering. However, there are two Medium findings: a semantic mismatch in the submit step profile name, and debug echo statements that print the resolved API base URL in plain CI logs.
Findings
Critical
None
High
None
Medium
[MEDIUM] Submit step uses --profile internal instead of --profile qa for internal-track QA builds
- dimension: Correctness (#1)
- location:
.github/workflows/android-release.yml:380-383 - issue: The
Submit build to Google Playstep passes--profile "${RELEASE_TRACK}"whereRELEASE_TRACKis the raw workflow input ("internal"). The PR adds asubmit.qaprofile ineas.jsonspecifically to handle the QA submission path, but the workflow never selects it — it selectssubmit.internalinstead. Todaysubmit.internalandsubmit.qaare identical (both map totrack: internal), so the immediate behavior is the same, but the newly-addedsubmit.qaprofile is dead and the intended coupling between theBUILD_EAS_PROFILE=qabuild path and theqasubmit profile is severed. - risk: Any future divergence between
submit.internalandsubmit.qa(e.g., adding areleaseStatus: draftor a different rollout config to QA) will silently fail to apply for workflow-triggered internal builds. The added profile is misleading — it implies the workflow uses it when it does not. - fix: Replace
--profile "${RELEASE_TRACK}"with--profile "${BUILD_EAS_PROFILE}"(already available inGITHUB_ENVfrom the Resolve step). Fortrack=internalthis resolves toqa; for all other tracks it resolves toproduction, which matches the existingsubmit.productionprofile. Update the step's env block:env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} BUILD_EAS_PROFILE: ${{ env.BUILD_EAS_PROFILE }} run: | set -euo pipefail npx eas-cli@latest submit \ --platform android \ --profile "${BUILD_EAS_PROFILE}" \ --path android/app/build/outputs/bundle/release/app-release.aab \ --non-interactive
- reference: CLAUDE.md rule 1 (root cause / no dead code coupling); CLAUDE.md rule 2 (
submit.qais unreachable from the workflow)
[MEDIUM] Debug echo statements print the resolved QA API base URL to plain CI log
- dimension: Security (#12)
- location:
.github/workflows/android-release.yml:205,239-240 - issue: Line 205 prints
"Build target: QA (api_base=${QA_API_BASE}, ...)"and lines 239-240 print"Prebuild API base: ${EXPO_PUBLIC_API_BASE}"unconditionally to the GitHub Actions log.QA_API_BASEis a repo variable (not a secret), so GitHub does not mask it — the URL appears in plaintext in every run log. - risk: Low immediate risk, but sets a precedent of printing infrastructure URLs to logs and could inadvertently expose the value if a future maintainer promotes it to a secret. The SKILL.md already documents the canonical QA URL, making the runtime log redundant.
- fix: Remove the three diagnostic echo lines. If Actions UI visibility is needed, scope to a GitHub notice annotation:
echo "::notice title=Build target::QA — test AdMob IDs enabled"
- reference: CLAUDE.md rule 4 (no logging in production pipelines); Security dimension — data exposure
Low / Info
[INFO] submit.internal and submit.qa are now identical — one is dead code
- dimension: Dead / stale code (#2)
- location:
apps/mobile/eas.json:53-66 - issue: Both profiles map to
track: internal, releaseStatus: completed. After applying the Medium fix (switch submit to--profile "${BUILD_EAS_PROFILE}"),submit.internalbecomes dead code. - fix: Apply the Medium fix, then remove
submit.internalfromeas.json. - reference: CLAUDE.md rule 2
[INFO] preview and qa build profiles now serve distinct purposes
- dimension: Correctness (#1)
- location:
apps/mobile/eas.json:19-37 - issue:
preview= prod API base + test AdMob;qa= QA API base + test AdMob. Not a bug, just worth noting so the profiles are not confused. - fix: No action required.
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | N/A — CI/CD and build config only; no app-code mirror required |
| i18n-syncer | N/A — no user-facing strings changed |
| contract-aligner | N/A — no packages/shared types or orbit-api DTOs changed |
| security-reviewer | N/A — no orbit-api code changed |
Validation
| Check | Result |
|---|---|
| Lint | N/A (no TS/JS/CS changed) |
| Type check | N/A |
| Tests | N/A |
| Build (api) | N/A |
| YAML/JSON parse | PASS (per PR author validation) |
What's good
- Fail-loud guard on unset
QA_API_BASE(exit 1with a clear, actionable error message) is exactly right — no silent fallback to prod. - Gating both prod AdMob validation steps with
if: inputs.track != 'internal'prevents false-positive failures on QA builds. - Environment injection in both the
prebuildandBuild signed AABsteps ensuresEXPO_PUBLIC_API_BASEis baked in at both the Expo config and Gradle layers. app.config.jsguard (assertProductionAdMobConfigchecksEAS_BUILD_PROFILE === "production") is respected:BUILD_EAS_PROFILE=qacorrectly bypasses the production AdMob enforcement.- SKILL.md QA documentation is a good addition for local development.
- The PR body is thorough: the A vs B derivation decision, the conflict with #232, and the operator runbook are all well documented.
Recommendation
Two items before merge:
- (Medium — fix before merge) Switch the submit step to
--profile "${BUILD_EAS_PROFILE}"so the newsubmit.qaprofile is actually used, then remove the deadsubmit.internalfromeas.json. - (Medium — fix or track) Remove the three diagnostic
echolines that print the resolved API URL and AdMob flag to the plain CI log.
Backward-compat guard is N/A: no packages/shared Zod schemas or orbit-api DTOs were touched.
🤖 Generated with Claude Code



What
Stands up the repo-side seam for a QA/staging environment that mirrors prod across web, api, and the Android internal track. The env-selection mechanism already exists on every surface; this PR feeds QA values into the one place that was missing it — the Android internal-track build — and makes QA a first-class build profile. Web and API need no code (both are env-driven); their QA instances are operator-provisioned (steps below).
Closes #211.
Code changes (this repo)
.github/workflows/android-release.yml— the core fix. The prebuild step never injectedEXPO_PUBLIC_API_BASE, so an internal-track build silently baked in the prod API base (https://api.useorbit.org). NewResolve build targetstep derives prod-vs-QA from the releasetrack:track == internal⇒ QA:EXPO_PUBLIC_API_BASEfromvars.QA_API_BASE,EXPO_PUBLIC_ADMOB_USE_TEST_IDS=true, eas profileqa. Fails loudly ifvars.QA_API_BASEis unset (no silent prod fallback).if: inputs.track != 'internal'so a QA build using test AdMob IDs doesn't trip them.apps/mobile/eas.json— newbuild.qaprofile (apk / internal distribution /EXPO_PUBLIC_API_BASE=https://api-qa.useorbit.org/ test AdMob), mirroring thepreviewshape, plus asubmit.qamapping to the Play internal track (for the directeas build/submit --profile qapath)..claude/skills/android-generate/SKILL.md— documents a QA switch: exportEXPO_PUBLIC_API_BASE(+ test AdMob) beforenpm run android:apk -w @orbit/mobile; default invocation still builds prod.apps/mobile/scripts/android-release-apk.js— intentionally unchanged. It already inheritsprocess.env, so the skill setting the env var is sufficient; adding a guard would be speculative (plan marked it optional).Target-derivation decision
The plan left open whether to derive the QA-vs-prod target from
track(A) or add an explicitapi_targetinput (B). Chose (A):track == internal⇒ QA. No new workflow input; the internal track is already the pre-prod track, so this is the lowest-friction mapping. Easy to switch to an explicit input later if a non-internal QA target is ever needed.Validation done
eas.jsonparses as valid JSON;qabuild +submit.qaprofiles present and mirror the existing profile shape.android-release.ymlparses as valid YAML; both prod-AdMob validation steps confirmed gated to non-internal tracks; the resolve + prebuild steps run unconditionally with the prebuild now reading the resolved env.#232also edits.github/workflows/android-release.yml. These edits will conflict at merge — resolve by hand so both changes survive: keep #232's changes and this PR'sResolve build targetstep + theif: inputs.track != 'internal'gates + the target-aware prebuild env block.Operator steps (NOT executed here — no cloud provisioning was done)
The QA web + API + DB are dashboard-provisioned.
ASPNETCORE_ENVIRONMENT=Productionfor the QA API is decided (faithful to prod; runs the strict validators), which means the full prod-grade secret set is required or the QA API won't boot.dotnet ef database update --connection "<QA pooler URI>"(the design-time factory reads prodappsettings.json, so pass--connectionexplicitly). Verify tables exist.orbit-apirepo + Dockerfile + branch. Note its public URL (https://<qa-api>.onrender.com).ASPNETCORE_ENVIRONMENT=Production) and attach to the QA API service. Required vars (ASP.NETSection__Keydouble-underscore):ConnectionStrings__DefaultConnection(QA pooler),Jwt__SecretKey(new 64-char random),Encryption__Key(new; must NOT contain "REPLACE"),Supabase__Url/Supabase__AnonKey(QA),Stripe__SecretKey/Stripe__PublishableKey/Stripe__WebhookSecret/Stripe__*PriceId*(TEST modesk_test_/pk_test_/whsec_+ test price IDs),Vapid__PublicKey/Vapid__PrivateKey(new QA pair).Firebase__CredentialsJson(single FCM projectorbit-11d4a),GooglePlay__ServiceAccountJson/GooglePlay__RtdnServiceAccountEmail(single Play apporg.useorbit.app).AI__ApiKey(OpenAI — no test mode),Resend__ApiKey(ideally sandbox domain).Cors__AllowedOrigins__0,Frontend__BaseUrl.apps/webwith env:API_BASE= QA API URL,NEXT_PUBLIC_SUPABASE_URL/_ANON_KEY= QA Supabase,NEXT_PUBLIC_VAPID_PUBLIC_KEY= QA VAPID public. Note its origin (e.g.qa.useorbit.org).Cors__AllowedOrigins__0+Frontend__BaseUrlon the QA API env group with the QA web origin; redeploy QA API.<QA API>stripe path; capturewhsec_+ test price IDs into the QA env group.Vapid__PrivateKey, public → both QA APIVapid__PublicKeyand QA webNEXT_PUBLIC_VAPID_PUBLIC_KEY.vars.QA_API_BASE=https://<qa-api>.onrender.com(orhttps://api-qa.useorbit.org) under repo Settings → Secrets and variables → Actions → Variables. The workflow injects this at prebuild; an internal run fails loudly if it's missing.org.useorbit.app(same app/package; no new Play app).api-qa.useorbit.org/qa.useorbit.orgCNAMEs to the Render services (matches the public hostnames used in the easqaprofile + skill).API repo
Per the plan, orbit-api needs 0 code — it's fully
builder.Configuration-driven, so QA = a Render env group + QA Supabase (operator steps 1-8 above). No orbit-api PR is opened (no real file change there). Arender.yamlblueprint-as-code was an explicit non-goal of this issue.Unblocks
#227 (QA smoke suite) hooks into the QA API health endpoint + QA web origin + QA internal build stood up here.
🤖 Generated with Claude Code