Skip to content

feat: return installed app metadata from apps install#274

Open
gmegidish wants to merge 1 commit into
mainfrom
feat/install-app-metadata
Open

feat: return installed app metadata from apps install#274
gmegidish wants to merge 1 commit into
mainfrom
feat/install-app-metadata

Conversation

@gmegidish

Copy link
Copy Markdown
Member

What

apps install now returns the installed app's identity and version metadata alongside the success message, parsed locally from the file (no device round-trips, no exec).

{ "status": "ok", "data": {
  "message": "Installed app from '…' on device …",
  "app": { "packageName": "com.mobilenext.playground", "version": "1.4.0", "versionCode": "42" }
}}

How

New utils.ParseAppMetadata(path) dispatches by extension:

  • .apk → pure-Go AXML parse via new dep github.com/shogo82148/androidbinary
  • .ipa / .zip → reads the top-level *.app/Info.plist from the archive via the existing howett.net/plist (handles both Payload/Foo.app/… and simulator-.zip Foo.app/… layouts; ignores nested framework/plugin plists)
  • .app dir → reads Info.plist directly

Unified field mapping across platforms:

field Android iOS
packageName package CFBundleIdentifier
version versionName CFBundleShortVersionString
versionCode versionCode CFBundleVersion

Metadata extraction is best-effort: a parse failure logs via utils.Verbose and omits app rather than failing a successful install. Parsing the original path works for all device types including remote (the file is local).

Tests

utils/appmeta_test.go (+ testdata/helloworld.apk) covers APK, IPA layout, simulator-zip layout, .app directory, nested-bundle exclusion, and unknown extension.

  • go build ./..., go vet, go test ./utils/ ./commands/ — pass

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Walkthrough

This PR adds cross-platform app metadata extraction by introducing a new ParseAppMetadata function and AppMetadata struct in the utils package. The implementation handles APK, IPA/ZIP, and APP directory formats with platform-specific parsers that extract package name, version, and version code. The InstallAppCommand is updated to return an InstallAppResult containing both a user message and optionally parsed app metadata. An androidbinary dependency is added to support APK parsing. The change includes comprehensive tests covering all supported formats and error cases.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: return installed app metadata from apps install' accurately describes the main change: adding metadata return to the apps install command.
Description check ✅ Passed The description clearly explains what was changed, how it was implemented, and provides testing context, all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/install-app-metadata

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
utils/appmeta_test.go (1)

77-89: 💤 Low value

Consider verifying all metadata fields for consistency.

The test correctly validates that nested framework plists don't override the app plist. For consistency with other tests, consider also asserting Version and VersionCode fields.

✨ Optional enhancement
 	meta, err := ParseAppMetadata(ipaPath)
 	require.NoError(t, err)
 	assert.Equal(t, "com.mobilenext.playground", meta.PackageName)
+	assert.Equal(t, "1.4.0", meta.Version)
+	assert.Equal(t, "42", meta.VersionCode)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/appmeta_test.go` around lines 77 - 89, Update the test
TestParseAppMetadataIgnoresNestedBundlePlists to assert the parsed Version and
VersionCode as well as PackageName: after calling ParseAppMetadata(ipaPath) and
obtaining meta, add assertions that meta.Version equals the expected version
string from sampleInfoPlist and that meta.VersionCode (or meta.VersionCodeInt if
that’s the actual field name) equals the expected numeric build/version code;
use require.NoError and assert.Equal like the existing PackageName check so the
test verifies all primary metadata fields are consistent with sampleInfoPlist.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@commands/apps.go`:
- Line 167: The code calls utils.ParseAppMetadata(req.Path) even when a resigned
IPA was installed; update the call to parse metadata from the actual installed
file by using installPath when req.ForceResign is true (fall back to req.Path
otherwise) — i.e., replace utils.ParseAppMetadata(req.Path) with
utils.ParseAppMetadata(installPathOrOriginal) where installPathOrOriginal =
installPath if req.ForceResign else req.Path (resignedPath is still available
due to the deferred removal).

In `@utils/appmeta_test.go`:
- Around line 111-114: The test TestParseAppMetadataRejectsUnknownExtension uses
a Unix-only path "/tmp/whatever.txt"; change the argument to ParseAppMetadata to
a simple relative filename (e.g., "whatever.txt" or "whatever.unknown") so the
test dispatches by extension without relying on an OS-specific temp directory;
update the call in TestParseAppMetadataRejectsUnknownExtension accordingly to
use the relative path.

---

Nitpick comments:
In `@utils/appmeta_test.go`:
- Around line 77-89: Update the test
TestParseAppMetadataIgnoresNestedBundlePlists to assert the parsed Version and
VersionCode as well as PackageName: after calling ParseAppMetadata(ipaPath) and
obtaining meta, add assertions that meta.Version equals the expected version
string from sampleInfoPlist and that meta.VersionCode (or meta.VersionCodeInt if
that’s the actual field name) equals the expected numeric build/version code;
use require.NoError and assert.Equal like the existing PackageName check so the
test verifies all primary metadata fields are consistent with sampleInfoPlist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8690475-503d-4218-bd9c-8126e91932ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0528f8a and 9216e24.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • commands/apps.go
  • go.mod
  • utils/appmeta.go
  • utils/appmeta_test.go
  • utils/testdata/helloworld.apk

Comment thread commands/apps.go
Comment thread utils/appmeta_test.go
Comment on lines +111 to +114
func TestParseAppMetadataRejectsUnknownExtension(t *testing.T) {
_, err := ParseAppMetadata("/tmp/whatever.txt")
assert.Error(t, err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a cross-platform path for the test.

The hardcoded /tmp/whatever.txt path is Unix-specific and will fail on Windows where /tmp/ doesn't exist in the same form. Since ParseAppMetadata dispatches by extension before attempting to open the file, use a simple relative path instead.

🔧 Proposed fix
 func TestParseAppMetadataRejectsUnknownExtension(t *testing.T) {
-	_, err := ParseAppMetadata("/tmp/whatever.txt")
+	_, err := ParseAppMetadata("whatever.txt")
 	assert.Error(t, err)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestParseAppMetadataRejectsUnknownExtension(t *testing.T) {
_, err := ParseAppMetadata("/tmp/whatever.txt")
assert.Error(t, err)
}
func TestParseAppMetadataRejectsUnknownExtension(t *testing.T) {
_, err := ParseAppMetadata("whatever.txt")
assert.Error(t, err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/appmeta_test.go` around lines 111 - 114, The test
TestParseAppMetadataRejectsUnknownExtension uses a Unix-only path
"/tmp/whatever.txt"; change the argument to ParseAppMetadata to a simple
relative filename (e.g., "whatever.txt" or "whatever.unknown") so the test
dispatches by extension without relying on an OS-specific temp directory; update
the call in TestParseAppMetadataRejectsUnknownExtension accordingly to use the
relative path.

@gmegidish gmegidish force-pushed the feat/install-app-metadata branch from 9216e24 to a500d86 Compare June 13, 2026 07:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
utils/appmeta.go (1)

66-96: 💤 Low value

Optional: Document first-match behavior for multiple app bundles.

The function returns the first matching top-level *.app/Info.plist found in the archive. For standard IPA/simulator-zip layouts, this is correct since only one app bundle should exist. If you want to make this explicit, consider adding a comment.

📝 Suggested documentation addition
 // parseIpaMetadata reads the top-level app's Info.plist from an .ipa or .zip
 // archive. It works for both .ipa (Payload/Foo.app/Info.plist) and simulator
-// .zip (Foo.app/Info.plist) layouts.
+// .zip (Foo.app/Info.plist) layouts. If multiple top-level app bundles exist,
+// the first match is returned.
 func parseIpaMetadata(path string) (*AppMetadata, error) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/appmeta.go` around lines 66 - 96, Add a short explanatory comment to
the parseIpaMetadata function documenting that it returns the first matching
top-level "*.app/Info.plist" found in the archive (first-match behavior) and
that this is appropriate for standard IPA/simulator-zip layouts where only one
app bundle is expected; also note that if multiple .app bundles exist the
function will ignore subsequent matches (and call out that changing this
behavior would require iterating all matches and merging/choosing among them).
Reference function parseIpaMetadata and place the comment immediately above its
declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/appmeta.go`:
- Around line 44-64: parseApkMetadata currently returns success even when
pkg.PackageName() is empty, producing degenerate AppMetadata; update
parseApkMetadata to validate that pkg.PackageName() is non-empty after opening
the APK and return a descriptive error (e.g., fmt.Errorf("apk has empty package
name")) if it is empty (ensure pkg is closed via the existing defer), so
AppMetadata.PackageName is never "" on success; keep the existing population of
Version/VersionCode logic unchanged.

---

Nitpick comments:
In `@utils/appmeta.go`:
- Around line 66-96: Add a short explanatory comment to the parseIpaMetadata
function documenting that it returns the first matching top-level
"*.app/Info.plist" found in the archive (first-match behavior) and that this is
appropriate for standard IPA/simulator-zip layouts where only one app bundle is
expected; also note that if multiple .app bundles exist the function will ignore
subsequent matches (and call out that changing this behavior would require
iterating all matches and merging/choosing among them). Reference function
parseIpaMetadata and place the comment immediately above its declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 674d1c9b-6147-4e08-8283-f3d95d056227

📥 Commits

Reviewing files that changed from the base of the PR and between 9216e24 and a500d86.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • commands/apps.go
  • go.mod
  • utils/appmeta.go
  • utils/appmeta_test.go
  • utils/testdata/sample.apk
✅ Files skipped from review due to trivial changes (1)
  • go.mod
🚧 Files skipped from review as they are similar to previous changes (2)
  • commands/apps.go
  • utils/appmeta_test.go

Comment thread utils/appmeta.go
Comment on lines +44 to +64
func parseApkMetadata(path string) (*AppMetadata, error) {
pkg, err := apk.OpenFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open apk: %w", err)
}
defer func() { _ = pkg.Close() }()

meta := &AppMetadata{
PackageName: pkg.PackageName(),
}

manifest := pkg.Manifest()
if versionName, err := manifest.VersionName.String(); err == nil {
meta.Version = versionName
}
if versionCode, err := manifest.VersionCode.Int32(); err == nil {
meta.VersionCode = strconv.FormatInt(int64(versionCode), 10)
}

return meta, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify androidbinary API compatibility

# Check the API exported by the library at v1.0.5
echo "=== Fetching androidbinary v1.0.5 API ==="
rg -A 3 'func.*PackageName|func.*Manifest|func.*VersionName|func.*VersionCode' \
  --iglob '**/androidbinary/**/*.go' 2>/dev/null || \
  echo "Note: Library not in current workspace; manual verification needed"

# Search Go package documentation
echo -e "\n=== Manual verification needed ==="
echo "Please verify the following at https://pkg.go.dev/github.com/shogo82148/androidbinary@v1.0.5/apk:"
echo "  1. apk.OpenFile(path) returns *Pkg with PackageName() method"
echo "  2. Pkg.Manifest() returns manifest with VersionName and VersionCode fields"  
echo "  3. VersionName has String() method, VersionCode has Int32() method"

Repository: mobile-next/mobilecli

Length of output: 517


Validate non-empty APK package name

parseApkMetadata always returns success and may set AppMetadata.PackageName to "" when pkg.PackageName() is empty; treat that as an error so callers don’t receive degenerate metadata.

🛡️ Suggested validation
 	meta := &AppMetadata{
 		PackageName: pkg.PackageName(),
 	}
+	if meta.PackageName == "" {
+		return nil, fmt.Errorf("APK manifest missing package name")
+	}
 
 	manifest := pkg.Manifest()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func parseApkMetadata(path string) (*AppMetadata, error) {
pkg, err := apk.OpenFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open apk: %w", err)
}
defer func() { _ = pkg.Close() }()
meta := &AppMetadata{
PackageName: pkg.PackageName(),
}
manifest := pkg.Manifest()
if versionName, err := manifest.VersionName.String(); err == nil {
meta.Version = versionName
}
if versionCode, err := manifest.VersionCode.Int32(); err == nil {
meta.VersionCode = strconv.FormatInt(int64(versionCode), 10)
}
return meta, nil
}
func parseApkMetadata(path string) (*AppMetadata, error) {
pkg, err := apk.OpenFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open apk: %w", err)
}
defer func() { _ = pkg.Close() }()
meta := &AppMetadata{
PackageName: pkg.PackageName(),
}
if meta.PackageName == "" {
return nil, fmt.Errorf("APK manifest missing package name")
}
manifest := pkg.Manifest()
if versionName, err := manifest.VersionName.String(); err == nil {
meta.Version = versionName
}
if versionCode, err := manifest.VersionCode.Int32(); err == nil {
meta.VersionCode = strconv.FormatInt(int64(versionCode), 10)
}
return meta, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/appmeta.go` around lines 44 - 64, parseApkMetadata currently returns
success even when pkg.PackageName() is empty, producing degenerate AppMetadata;
update parseApkMetadata to validate that pkg.PackageName() is non-empty after
opening the APK and return a descriptive error (e.g., fmt.Errorf("apk has empty
package name")) if it is empty (ensure pkg is closed via the existing defer), so
AppMetadata.PackageName is never "" on success; keep the existing population of
Version/VersionCode logic unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant