From 41c31f01a34dc73c815d12ca26ac510ff6a4fd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Fri, 24 Apr 2026 17:02:55 +0200 Subject: [PATCH] Skip unknown entitlement keys with a warning in SyncBundleID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple routinely adds new entitlement keys (most recently `com.apple.developer.healthkit.access`, `kernel.increased-memory-limit`, etc.). Any key not present in `ServiceTypeByKey` causes `Entitlement.Capability()` to hard-error and `SyncBundleID` to abort the entire code signing phase, which means a single freshly-introduced key breaks every build that happens to declare it until the allow-list is extended and a new step release is cut. This change: - Introduces `autocodesign.ErrUnknownEntitlementKey` as an exported sentinel, and wraps it with `fmt.Errorf("%w: %s", …)` in both `Capability()` and `Equal()`. Callers that have existing `errors.Is(err, ErrUnknownEntitlementKey)` handling continue to work; the human-readable message is unchanged (`unknown entitlement key: `). - Updates `ProfileClient.SyncBundleID` to catch the sentinel, log a warning identifying the key, and continue syncing the remaining entitlements instead of aborting. Other entitlements in the project still get registered correctly. Tradeoff: if Apple adds an entitlement that genuinely requires App Store Connect registration and it is not yet in `ServiceTypeByKey`, the step will log a warning and move on rather than erroring out. The resulting provisioning profile will miss that one capability — but every other capability on the bundle ID is now synced correctly, instead of the current behavior where *no* capability gets synced. A missing capability surfaces as a later-stage provisioning error with clearer attribution than today's opaque `unknown entitlement key` message. Adds a regression test (`TestCapability_UnknownKeyReturnsSentinel`) covering sentinel wrapping and key attribution. --- .../appstoreconnectclient/profiles.go | 4 ++++ autocodesign/entitlement.go | 12 ++++++++++-- autocodesign/entitlement_test.go | 13 +++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/autocodesign/devportalclient/appstoreconnectclient/profiles.go b/autocodesign/devportalclient/appstoreconnectclient/profiles.go index 46ece7da..32683e04 100644 --- a/autocodesign/devportalclient/appstoreconnectclient/profiles.go +++ b/autocodesign/devportalclient/appstoreconnectclient/profiles.go @@ -399,6 +399,10 @@ func (c *ProfileClient) SyncBundleID(bundleID appstoreconnect.BundleID, appEntit ent := autocodesign.Entitlement{key: value} cap, err := ent.Capability() if err != nil { + if errors.Is(err, autocodesign.ErrUnknownEntitlementKey) { + log.Warnf("Skipping unknown entitlement key %q while syncing bundle ID capabilities. If this is a recognised Apple entitlement that should be registered on App Store Connect, please report it to bitrise-io/go-xcode so it can be added to ServiceTypeByKey.", key) + continue + } return err } if cap == nil { diff --git a/autocodesign/entitlement.go b/autocodesign/entitlement.go index cde18e67..78c0a54b 100644 --- a/autocodesign/entitlement.go +++ b/autocodesign/entitlement.go @@ -13,6 +13,14 @@ import ( // ICloudIdentifiersEntitlementKey ... const ICloudIdentifiersEntitlementKey = "com.apple.developer.icloud-container-identifiers" +// ErrUnknownEntitlementKey is returned by Entitlement.Capability and +// Entitlement.Equal when an entitlement key is not present in +// appstoreconnect.ServiceTypeByKey. Callers that iterate over a project's +// full entitlement set (e.g. SyncBundleID) may wrap this with errors.Is to +// skip unknown keys with a warning instead of aborting, so a single new +// Apple-introduced key does not break the entire code signing flow. +var ErrUnknownEntitlementKey = errors.New("unknown entitlement key") + // DataProtections ... var DataProtections = map[string]appstoreconnect.CapabilityOptionKey{ "NSFileProtectionComplete": appstoreconnect.CompleteProtection, @@ -44,7 +52,7 @@ func (e Entitlement) Capability() (*appstoreconnect.BundleIDCapability, error) { capType, ok := appstoreconnect.ServiceTypeByKey[entKey] if !ok { - return nil, errors.New("unknown entitlement key: " + entKey) + return nil, fmt.Errorf("%w: %s", ErrUnknownEntitlementKey, entKey) } if capType == appstoreconnect.Ignored { @@ -144,7 +152,7 @@ func (e Entitlement) Equal(cap appstoreconnect.BundleIDCapability, allEntitlemen capType, ok := appstoreconnect.ServiceTypeByKey[entKey] if !ok { - return false, errors.New("unknown entitlement key: " + entKey) + return false, fmt.Errorf("%w: %s", ErrUnknownEntitlementKey, entKey) } if cap.Attributes.CapabilityType != capType { diff --git a/autocodesign/entitlement_test.go b/autocodesign/entitlement_test.go index 61ac5e9b..b90a6cb0 100644 --- a/autocodesign/entitlement_test.go +++ b/autocodesign/entitlement_test.go @@ -1,6 +1,7 @@ package autocodesign import ( + "errors" "testing" "github.com/stretchr/testify/require" @@ -109,6 +110,18 @@ func TestCapability_HealthKitAccessIgnored(t *testing.T) { require.Nil(t, cap) } +func TestCapability_UnknownKeyReturnsSentinel(t *testing.T) { + ent := Entitlement(map[string]interface{}{ + "com.apple.developer.totally-made-up-entitlement": true, + }) + + cap, err := ent.Capability() + require.Error(t, err) + require.True(t, errors.Is(err, ErrUnknownEntitlementKey)) + require.Contains(t, err.Error(), "com.apple.developer.totally-made-up-entitlement") + require.Nil(t, cap) +} + func TestCapability_IgnoredKeys(t *testing.T) { keys := []string{ "com.apple.developer.kernel.extended-virtual-addressing",