Implement Android TV Play plan target#456
Conversation
Greptile SummaryThis PR converts the Android TV target into an inspectable Google Play packaging plan, adding optional manifest validation (Leanback feature + LEANBACK_LAUNCHER), a configurable Gradle task, and store metadata in the real ship response.
Confidence Score: 3/5Not safe to merge as-is — manifest validation is incomplete and the ship path performs unnecessary disk I/O that will fail in typical CD environments. The validation function promises three manifest requirements but only enforces two, so invalid TV manifests pass silently. The ship method unconditionally re-reads the manifest from disk on every real upload, where the project source is rarely present, turning a successful build into a broken deploy. Both changed files need attention: index.ts for the validation gap and the unconditional buildPlan call in ship(); index.test.ts for the positive-path manifest fixture that will fail once the validation is fixed. Important Files Changed
Sequence DiagramsequenceDiagram
participant CI
participant build as adapter.build()
participant ship as adapter.ship()
participant fs as File System
participant play as Play Console API
CI->>build: build(ctx, config)
build->>fs: maybeValidateManifest (reads AndroidManifest.xml)
fs-->>build: manifest content
build->>build: validateAndroidTvManifest()
build->>fs: writeFile(androidtv-package-plan.json)
build-->>CI: "{ artifact, meta: { planFile, track, manifestChecks } }"
CI->>ship: ship(ctx, config)
ship->>build: buildPlan(ctx, config) [always called]
build->>fs: maybeValidateManifest (re-reads manifest)
fs-->>build: manifest / ENOENT error
alt dryRun
ship-->>CI: "{ id: dry-run, meta: { commands } }"
else live upload
ship->>play: [TODO] edits.insert / upload / commit
ship-->>CI: "{ id, url, meta: { artifact, track } }"
end
Reviews (1): Last reviewed commit: "Implement Android TV Play plan target" | Re-trigger Greptile |
| if (!manifest.includes('android.intent.category.LEANBACK_LAUNCHER')) { | ||
| throw new Error('AndroidManifest must declare LEANBACK_LAUNCHER'); | ||
| } | ||
| } |
There was a problem hiding this comment.
The
validateAndroidTvManifest function checks for android.software.leanback and LEANBACK_LAUNCHER but silently skips the android.intent.category.LAUNCHER check that manifestChecks marks as required: true. Any manifest that lacks the LAUNCHER category will pass validation and produce a plan file that contradicts its own manifestChecks entries — the app would be unpublishable to the standard Play Store launcher.
| if (!manifest.includes('android.intent.category.LEANBACK_LAUNCHER')) { | |
| throw new Error('AndroidManifest must declare LEANBACK_LAUNCHER'); | |
| } | |
| } | |
| if (!manifest.includes('android.intent.category.LEANBACK_LAUNCHER')) { | |
| throw new Error('AndroidManifest must declare LEANBACK_LAUNCHER'); | |
| } | |
| if (!manifest.includes('android.intent.category.LAUNCHER')) { | |
| throw new Error('AndroidManifest must declare android.intent.category.LAUNCHER'); | |
| } | |
| } |
| @@ -92,8 +125,12 @@ export default defineTarget<Config>({ | |||
| } | |||
There was a problem hiding this comment.
In the non-dry-run branch of
ship(), buildPlan() is awaited but its return value is never used — plan goes out of scope immediately after the if block. Because buildPlan calls maybeValidateManifest, which reads the manifest from disk, this means every real upload triggers a gratuitous file-system read that will throw ENOENT whenever the project source is not co-located with the ship environment (e.g., a separate CD runner that only has the artifact). Moving buildPlan inside the if (ctx.dryRun) guard keeps the dry-run behavior intact without the risk.
| async ship(ctx, config) { | |
| const track = releaseTrack(ctx.channel, config); | |
| const packageName = requirePackageName(config); | |
| ctx.log(`upload to Play Console package=${packageName} track=${track}`); | |
| if (ctx.dryRun) { | |
| const plan = await buildPlan(ctx, config); | |
| return { | |
| id: 'dry-run', | |
| meta: { | |
| packageName, | |
| artifact: ctx.artifact, | |
| track, | |
| commands: plan.commands.slice(1), | |
| }, | |
| }; | |
| } |
| await writeFile(join(projectDir, 'app', 'src', 'main', 'AndroidManifest.xml'), ` | ||
| <manifest package="com.acme.tv"> | ||
| <uses-feature android:name="android.software.leanback" android:required="true" /> | ||
| <application> | ||
| <activity android:name=".MainActivity"> | ||
| <intent-filter> | ||
| <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
| </application> | ||
| </manifest> | ||
| `, 'utf-8'); |
There was a problem hiding this comment.
The "validates a provided manifest" test fixture does not include
android.intent.category.LAUNCHER, yet the test expects the plan to be written successfully. Once the missing validation check in validateAndroidTvManifest is added, this test will start failing — the fixture needs a LAUNCHER intent filter to remain a valid positive-path test.
Summary
Tests
pnpm --filter @profullstack/sh1pt-target-tv-androidtv typecheckpnpm test -- packages/targets/tv-androidtv/src/index.test.ts