Skip to content

feat(publish): wire watchOS publishing + tvOS/watchOS App Store distribute#5085

Merged
proggeramlug merged 2 commits into
mainfrom
feat/tvos-watchos-publish
Jun 13, 2026
Merged

feat(publish): wire watchOS publishing + tvOS/watchOS App Store distribute#5085
proggeramlug merged 2 commits into
mainfrom
feat/tvos-watchos-publish

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What

Adds first-class watchOS publishing and App Store Connect distribute for both tvOS and watchOS to perry publish. watchOS previously had no publish path (the [watchos] manifest wiring was missing); tvOS could build but had no distribute option.

Companion changes (separate): hub PR (license + advertise tvos/watchos) and worker deploy patches (build/sign/upload). This PR is the CLI half.

Changes

  • server_api.rsBuildManifest: add tvos_distribute + watchos_{deployment_target,encryption_exempt,info_plist,distribute}.
  • config_types.rsdistribute on TvosConfig/WatchosConfig; entry on WatchosConfig.
  • mod.rsis_watchos; watchOS entry (src/main_watchos.ts) + bundle-id resolution; build_number gate; tvOS/watchOS distribute extraction + manifest population; target_display + publish summary.
  • credentials.rs — tvOS/watchOS appstore/testflight credential preflight (App Store Connect key id / issuer / .p8); watchOS added to the interactive target picker.
  • preflight.rs — classify watchOS as a gui app.

Standalone watchOS bundle id

perry publish watchos ships an independent/standalone watchOS app, which Apple requires to have its own unique bundle id. The watchOS resolver therefore does not fall back to the iOS bundle id (unlike tvOS), and appstore/testflight publishing hard-requires an explicit [watchos] bundle_id — otherwise App Store Connect rejects the duplicate id.

Notes

  • watchOS device builds target arm64_32 (ILP32) per the existing perry watchOS toolchain; ABI selection is a worker/CI concern, not this PR.
  • tvOS/watchOS reuse the iOS ios-precompiled → ios-sign build/sign path (they sign/package exactly like iOS), so no new server capability is introduced.

Test

cargo check -p perry passes. Manual: perry publish watchos/tvos on a sample app builds the expected manifest (targets, watchos_*/tvos_* fields) and rejects missing App Store Connect creds / missing [watchos] bundle_id.

Summary by CodeRabbit

  • New Features

    • End-to-end watchOS publishing support (App Store & TestFlight), including packaging/signing metadata.
    • Build manifest and publish summary now include watchOS-specific fields and status.
  • Bug Fixes / Validation

    • Stronger credential checks for tvOS and watchOS distributions; watchOS standalone bundle-id is now enforced.
  • Tests

    • Added tests covering tvOS/watchOS distribution credential requirements and edge cases.

…ibute

watchOS had no publish path; this mirrors the tvOS wiring for watchOS and
adds App Store Connect distribute to both tvOS and watchOS.

- server_api: add tvos_distribute + watchos_{deployment_target,
  encryption_exempt,info_plist,distribute} to BuildManifest
- config_types: add distribute to TvosConfig/WatchosConfig; entry to WatchosConfig
- mod.rs: is_watchos flag; watchOS entry (src/main_watchos.ts) + bundle_id
  resolution; build_number gate; tvos/watchos distribute extraction +
  manifest population; target_display + summary
- credentials: tvOS/watchOS appstore/testflight credential preflight;
  watchOS added to interactive target picker
- preflight: classify watchOS as a gui app

A standalone watchOS app must have its own unique bundle id, so the watchOS
resolver does NOT fall back to the iOS bundle id, and appstore/testflight
publishing hard-requires an explicit [watchos] bundle_id (App Store Connect
rejects duplicates).
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR extends the perry publish command to recognize watchOS as a target platform. It adds watchOS configuration options, integrates watchOS into platform selection prompts, enforces credential validation for App Store Connect distribution, and wires watchOS through entry-point resolution, bundle-ID assignment, build-number auto-increment, and build-manifest serialization to the build server.

Changes

watchOS Publish Support

Layer / File(s) Summary
Configuration schema and documentation
crates/perry/src/commands/publish/config_types.rs
WatchosConfig and TvosConfig struct fields are documented and reordered to clarify how the distribute setting controls App Store Connect and TestFlight uploads.
Platform selection and user prompting
crates/perry/src/commands/publish/credentials.rs (lines 5–14, 27–29)
Target platform prompt is expanded to include watchOS alongside iOS, macOS, Android, tvOS, and visionOS; platform-to-string matching is updated to route the watchOS selection through the flow.
Credential validation for tvOS and watchOS
crates/perry/src/commands/publish/credentials.rs (lines 264–267, 317–366)
validate_credentials_for_distribute function signature is extended with tvOS and watchOS distribution flags, and validation logic is added to enforce Apple Connect key ID, issuer ID, and signing key requirements when tvOS or watchOS targets App Store Connect or TestFlight.
Entry and bundle-id resolution helpers
crates/perry/src/commands/publish/resolve.rs
Adds resolve_entry and resolve_bundle_id helpers that prefer platform-specific config (including [watchos]) and implement watchOS-specific bundle-id behavior (no iOS fallback for standalone watchOS App Store uploads).
Publish workflow integration
crates/perry/src/commands/publish/mod.rs (multiple ranges)
Wires watchOS into target flags, uses resolve_entry/resolve_bundle_id, enforces standalone watchOS bundle-id for App Store uploads, updates build-number/macOS flag logic, forwards tvos_distribute/watchos_distribute to credential preflight, and includes watchOS in summary output.
Build server manifest schema for watchOS
crates/perry/src/commands/publish/server_api.rs
BuildManifest struct gains four optional watchOS fields (watchos_deployment_target, watchos_encryption_exempt, watchos_info_plist, watchos_distribute) that are serialized only when present.
Security audit classification for watchOS
crates/perry/src/commands/publish/preflight.rs (line 53)
watchOS is classified alongside other Apple GUI-like platforms (iOS, tvOS, macOS, visionOS) in the security audit step, resolving to "gui" app type instead of server default.
Tests: credential validation and placeholders
crates/perry/src/commands/publish/tests.rs
Tests updated to pass tvOS/watchOS placeholder args and new tests added for tvOS/watchOS credential validation scenarios (App Store requirements, missing issuer, and no-distribute pass).

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A watch to wear and a choice to share,
bundleId checks show we deeply care,
From toml to manifest, credentials align,
WatchOS joins perry—now signatures shine! ⌚✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: wiring watchOS publishing and adding App Store distribute support for tvOS and watchOS.
Description check ✅ Passed The description provides a comprehensive overview with clear sections covering What, Changes, standalone watchOS bundle ID behavior, notes, and test results against the template requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tvos-watchos-publish

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry/src/commands/publish/mod.rs (1)

1095-1110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

WatchOS-specific signing overrides are still ignored by publish.

This new preflight is watchOS-aware, but the Apple credential resolution above still only reads TOML values from [ios] or [macos]. crates/perry/src/commands/publish/config_types.rs:166-180 defines [watchos].team_id and [watchos].signing_identity, so perry publish watchos currently ignores those overrides and silently falls back to macOS/global credentials.

Either wire watchOS to its own TOML overrides or explicitly delegate it to the iOS config path; the current macOS fallback is the wrong contract for a standalone watchOS publish flow.

🤖 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 `@crates/perry/src/commands/publish/mod.rs` around lines 1095 - 1110, The
publish flow is ignoring watchOS-specific TOML overrides ([watchos].team_id,
[watchos].signing_identity); update the credential resolution so
validate_credentials_for_distribute receives watchOS values instead of silently
falling back to macOS. Concretely, when is_watchos is true pass the watchOS
distribute config (watchos_distribute.as_deref()) or explicitly delegate to the
iOS path (ios_distribute.as_deref()) depending on the intended contract, so that
validate_credentials_for_distribute(...) sees the watchOS overrides rather than
macOS/global values.
🤖 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 `@crates/perry/src/commands/publish/mod.rs`:
- Around line 307-328: The code increments build_number (computed from
toml_build_number and written back to perry_toml_path) for tvOS/watchOS but does
not propagate that new value into the app manifest (CFBundleVersion) because the
manifest serialization currently only applies to iOS/visionOS/Android/macOS;
update the manifest-write logic so the same build_number (the variable
build_number / n) is written into CFBundleVersion for tvOS and watchOS as well
(i.e., include tvOS/watchOS in the branch that serializes CFBundleVersion or
explicitly set CFBundleVersion = build_number when handling tvOS/watchOS) so the
worker receives the incremented CFBundleVersion used for upload.

---

Outside diff comments:
In `@crates/perry/src/commands/publish/mod.rs`:
- Around line 1095-1110: The publish flow is ignoring watchOS-specific TOML
overrides ([watchos].team_id, [watchos].signing_identity); update the credential
resolution so validate_credentials_for_distribute receives watchOS values
instead of silently falling back to macOS. Concretely, when is_watchos is true
pass the watchOS distribute config (watchos_distribute.as_deref()) or explicitly
delegate to the iOS path (ios_distribute.as_deref()) depending on the intended
contract, so that validate_credentials_for_distribute(...) sees the watchOS
overrides rather than macOS/global values.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 028c9110-e83a-4b9a-b286-827acd6cab66

📥 Commits

Reviewing files that changed from the base of the PR and between e092362 and dbc8799.

📒 Files selected for processing (5)
  • crates/perry/src/commands/publish/config_types.rs
  • crates/perry/src/commands/publish/credentials.rs
  • crates/perry/src/commands/publish/mod.rs
  • crates/perry/src/commands/publish/preflight.rs
  • crates/perry/src/commands/publish/server_api.rs

Comment on lines +307 to +328
let build_number =
if is_ios || is_visionos || is_tvos || is_watchos || is_android || macos_needs_upload {
let n = toml_build_number + 1;
if let Ok(content) = fs::read_to_string(&perry_toml_path) {
let updated = if content.contains("build_number =") {
content.replace(
&format!("build_number = {}", toml_build_number),
&format!("build_number = {}", n),
)
} else {
// Insert build_number after the version line
content.replace(
&format!("version = \"{}\"", version),
&format!("version = \"{}\"\nbuild_number = {}", version, n),
)
};
fs::write(&perry_toml_path, &updated).ok();
}
n
} else {
toml_build_number
};

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

Propagate the new tvOS/watchOS build number into the manifest too.

This block now increments build_number for tvOS/watchOS, but Lines 1281-1286 still serialize build-number semantics only for iOS/visionOS/Android/macOS uploads. The worker therefore receives the marketing version for tvOS/watchOS, so a second App Store/TestFlight publish can reuse the old CFBundleVersion and get rejected.

Suggested fix
-    let (manifest_version, manifest_short_version) =
-        if is_ios || is_visionos || is_android || macos_needs_upload {
+    let (manifest_version, manifest_short_version) =
+        if is_ios || is_visionos || is_tvos || is_watchos || is_android || macos_needs_upload {
             (build_number.to_string(), Some(version.clone()))
         } else {
             (version.clone(), None)
         };
🤖 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 `@crates/perry/src/commands/publish/mod.rs` around lines 307 - 328, The code
increments build_number (computed from toml_build_number and written back to
perry_toml_path) for tvOS/watchOS but does not propagate that new value into the
app manifest (CFBundleVersion) because the manifest serialization currently only
applies to iOS/visionOS/Android/macOS; update the manifest-write logic so the
same build_number (the variable build_number / n) is written into
CFBundleVersion for tvOS and watchOS as well (i.e., include tvOS/watchOS in the
branch that serializes CFBundleVersion or explicitly set CFBundleVersion =
build_number when handling tvOS/watchOS) so the worker receives the incremented
CFBundleVersion used for upload.

…st call sites

- Extract per-target entry/bundle_id resolution from mod.rs into resolve.rs
  (mod.rs was 2039 lines, over the 2000-line lint gate -> now 1954).
- validate_credentials_for_distribute gained 4 params (is_tvos/tvos_distribute,
  is_watchos/watchos_distribute); update the 10 #[cfg(test)] call sites that
  cargo check skips but cargo test compiles, and add tvOS/watchOS credential
  validation tests.

@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.

🧹 Nitpick comments (2)
crates/perry/src/commands/publish/tests.rs (1)

266-560: ⚡ Quick win

Reduce positional-argument brittleness in credential validation tests.

These tests repeatedly pass long bool/Option tuples, which is easy to misorder as the validator signature evolves. Consider a tiny test helper (or per-platform wrappers) to centralize defaults and make each case intent-focused.

Refactor sketch
+fn validate_ios_case(
+    ios_distribute: Option<&str>,
+    apple_key_id: Option<&str>,
+    apple_issuer_id: Option<&str>,
+    p8_key_content: Option<&str>,
+) -> anyhow::Result<()> {
+    validate_credentials_for_distribute(
+        false, None, None, // android
+        true, ios_distribute, apple_key_id, apple_issuer_id, p8_key_content, // ios
+        false, None, // macos
+        false, None, // tvos
+        false, None, // watchos
+    )
+}
🤖 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 `@crates/perry/src/commands/publish/tests.rs` around lines 266 - 560, The tests
in tests.rs call validate_credentials_for_distribute with long positional
bool/Option tuples making them brittle; create a small test helper (e.g., a
builder or wrapper function test_validate_args or per-platform helpers like
android_args, ios_args, macos_args) that supplies sensible defaults and accepts
only the values each test cares about, then update each test to call the helper
with named parameters (or a short struct) and pass the resulting fields into
validate_credentials_for_distribute; reference the existing
validate_credentials_for_distribute call sites to replace direct long tuples
with helper-produced arguments, and keep helper code local to the tests module
to avoid touching production code.
crates/perry/src/commands/publish/resolve.rs (1)

79-79: ⚡ Quick win

Consider sanitizing app_name more thoroughly for bundle ID generation.

The default bundle ID generator only replaces spaces with hyphens but doesn't sanitize other special characters. If app_name contains characters like @, #, !, or Unicode, the generated bundle ID com.perry.{app_name} could be invalid (bundle IDs should only contain alphanumeric characters, hyphens, and periods).

♻️ Proposed sanitization improvement
-    let default = || format!("com.perry.{}", app_name.to_lowercase().replace(' ', "-"));
+    let default = || {
+        let sanitized = app_name
+            .to_lowercase()
+            .chars()
+            .map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c } else { '-' })
+            .collect::<String>();
+        format!("com.perry.{}", sanitized)
+    };
🤖 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 `@crates/perry/src/commands/publish/resolve.rs` at line 79, The bundle ID
default closure currently only replaces spaces; update it to robustly sanitize
app_name: create/inline a small sanitizer used by the default closure that
lowercases app_name, normalizes or strips/transliterates diacritics if
available, replaces any character not in [a-z0-9-] with a hyphen (using a regex
like [^a-z0-9-]+), collapses consecutive hyphens into one, and trims
leading/trailing hyphens so the resulting format is safe for
"com.perry.{sanitized}". Modify the existing closure (the local variable default
that references app_name) to call this sanitizer before building the final
string; ensure the sanitizer function name (e.g., sanitize_bundle_component) is
unique and used by the default closure.
🤖 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.

Nitpick comments:
In `@crates/perry/src/commands/publish/resolve.rs`:
- Line 79: The bundle ID default closure currently only replaces spaces; update
it to robustly sanitize app_name: create/inline a small sanitizer used by the
default closure that lowercases app_name, normalizes or strips/transliterates
diacritics if available, replaces any character not in [a-z0-9-] with a hyphen
(using a regex like [^a-z0-9-]+), collapses consecutive hyphens into one, and
trims leading/trailing hyphens so the resulting format is safe for
"com.perry.{sanitized}". Modify the existing closure (the local variable default
that references app_name) to call this sanitizer before building the final
string; ensure the sanitizer function name (e.g., sanitize_bundle_component) is
unique and used by the default closure.

In `@crates/perry/src/commands/publish/tests.rs`:
- Around line 266-560: The tests in tests.rs call
validate_credentials_for_distribute with long positional bool/Option tuples
making them brittle; create a small test helper (e.g., a builder or wrapper
function test_validate_args or per-platform helpers like android_args, ios_args,
macos_args) that supplies sensible defaults and accepts only the values each
test cares about, then update each test to call the helper with named parameters
(or a short struct) and pass the resulting fields into
validate_credentials_for_distribute; reference the existing
validate_credentials_for_distribute call sites to replace direct long tuples
with helper-produced arguments, and keep helper code local to the tests module
to avoid touching production code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 85048080-e744-4b05-95d8-c54b75e9bfa7

📥 Commits

Reviewing files that changed from the base of the PR and between dbc8799 and 46ab408.

📒 Files selected for processing (3)
  • crates/perry/src/commands/publish/mod.rs
  • crates/perry/src/commands/publish/resolve.rs
  • crates/perry/src/commands/publish/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry/src/commands/publish/mod.rs

@proggeramlug proggeramlug merged commit 9b6bbe0 into main Jun 13, 2026
13 of 14 checks passed
@proggeramlug proggeramlug deleted the feat/tvos-watchos-publish branch June 13, 2026 13:13
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