Skip to content

Let xcodebuild authenticate with an App Store Connect API key (1.10.0) - #74

Merged
MAUstaoglu merged 9 commits into
fluttertv:devfrom
MAUstaoglu:fix/xcodebuild-asc-api-key
Sep 4, 2026
Merged

Let xcodebuild authenticate with an App Store Connect API key (1.10.0)#74
MAUstaoglu merged 9 commits into
fluttertv:devfrom
MAUstaoglu:fix/xcodebuild-asc-api-key

Conversation

@MAUstaoglu

@MAUstaoglu MAUstaoglu commented Sep 3, 2026

Copy link
Copy Markdown
Member

Targets dev per CONTRIBUTING.md.

Version: 1.9.0 → 1.10.0, at the maintainer's request. Minor rather than patch — the API-key path is a new capability and is additive. For the record, CONTRIBUTING.md asks contributors to leave the version alone and file under [Unreleased]; this is that call being made deliberately, not the convention being missed.

Revised after review. Two claims in the original description were wrong and have been removed rather than edited quietly: that only the key id is ever logged, and that the tests drove every branch. A third — a "58 pre-existing failures" baseline — was an artifact of running the suite without the resolved TMPDIR CI passes. Details in the thread.

The problem

-allowProvisioningUpdates lets Xcode create or refresh a provisioning profile, but on its own it can only do so through a signed-in Xcode account. On a machine without a usable one — CI, or a developer who only has an API key — xcodebuild cannot fetch the profile and quietly settles for a cached wildcard one instead.

Any app with an entitlement then fails, and fails misleadingly:

error: Provisioning profile "tvOS Team Provisioning Profile: *"
doesn't include the Game Center capability.

That names the capability the wildcard lacks, not the credential that is actually missing. The obvious next step is to go auditing the App ID's capabilities in the developer portal — where everything is already correct.

How it came up

An Apple TV device build of an app carrying com.apple.developer.game-center failed with exactly that. The App ID was UNIVERSAL and already had GAME_CENTER enabled; querying the App Store Connect API showed the real gap was that no TVOS_APP_DEVELOPMENT profile existed for that bundle at all — iOS, App Store, watch and Mac profiles were all present.

With the key forwarded, xcodebuild created the missing profile and the same build signed, installed and launched on the device unchanged.

The change

Forward -authenticationKeyPath / -authenticationKeyID / -authenticationKeyIssuerID when the environment supplies all three:

export APP_STORE_CONNECT_KEY_PATH=~/.appstoreconnect/private_keys/AuthKey_XXXX.p8
export APP_STORE_CONNECT_KEY_ID=XXXXXXXXXX
export APP_STORE_CONNECT_ISSUER_ID=<issuer-uuid>

Additive. Set none of them and nothing is added: a machine with a working Xcode account behaves exactly as before, silently.

Anything between that and a working setup is reported. xcodebuild requires all three or none, so a partial set is not forwarded — but it is not swallowed either. The gate tests presence in the environment, not a non-empty value, because an undefined CI secret interpolates to the empty string rather than to an absent variable: all three are set, the operator has demonstrably configured this, and treating it as unconfigured loses the only signal there is. The warning names the missing variable, including the APP_STORE_CONNECT_KEY_ISSUER_ID guess that the asymmetric naming invites. A key path that does not exist warns too, and says so when a literal ~ is the reason.

Input handling. Values are trimmed — KEY_ID=$(cat keyid) and most secret stores append a newline, and in a path the invisible \n renders a warning whose path looks perfectly correct. A leading ~ is expanded against HOME: an interactive shell expands it before the CLI sees it, but a CI env: block, a quoted assignment, a .env file and an Xcode scheme variable all deliver it literally. The path is made absolute, because existsSync resolves against the CLI's cwd while xcodebuild runs with workingDirectory: tvosProjectDir.path.

Output. A successful handover names the key id at default verbosity, so "working", "half-configured" and "never configured" can be told apart from the log. The .p8 contents never pass through the CLI — only the path is read, and xcodebuild opens the file itself. The path, key id and issuer id are not secret and do appear in output: on that success line, and in xcodebuild's own echoed command line, which the caller dumps to printError on a failed build.

The args are resolved before startProgress, not inside the argument list, so a warning is not emitted at the top of a multi-minute build and then buried under xcodebuild's stdout and stderr — the same reasoning the migration guards below already follow.

Tests

17 cases in test/general/tvos_code_signing_test.dart covering the forwarding path, each way the trio can be incomplete or empty, trimming, tilde expansion with and without HOME, relative-path resolution, the missing-file fallback, and the success log.

resolveAuthenticationArgs takes its environment, file system and logger as arguments, so those drive it directly. The call site needed extracting to be reachable at all: NativeTvosBundle.xcodebuildArgs now assembles the argv, following the existing aotAssembleArgs / aotLinkArgs pattern, and is tested for forwarding the signing and authentication args on a device build and neither on a simulator. Without it the feature could be unhooked from the build entirely with the suite still green.

Also drops the two cases in Code signing - simulator vs device, which asserted expect(const <String>[], isEmpty) and expect(isSimulator, isFalse) against local constants and could not fail. The new group makes both claims against production code.

Suite is green, run as CI runs it: dev (cc9caf5) 431 passing / 0 failing, this branch 446 / 0 — the +15 being the net change here. dart analyze --fatal-infos lib/ test/ bin/ clean.

Also included: two docs fixes

README.md documented none of the three variables — they appeared only in CHANGELOG.md and the dartdoc, so a user was guessing them from the xcodebuild flag names, and guessing wrong on the issuer. They are now in the Code signing section alongside the engine-signing ones.

CONTRIBUTING.md: the documented test command omitted the resolved TMPDIR that .github/workflows/test.yml passes and test/README.md explains, so anyone following it hits ~58 phantom failures from the FS guard's symlink mismatch — which is exactly how the bogus baseline above got into this description. Prefix added, and the stale "74 unit tests" corrected (there are ~450 across 40 files).

It also gains a note on a gotcha that cost me a build cycle: shared.sh only recompiles bin/cache/flutter-tvos.snapshot when git rev-parse HEAD changes, so an uncommitted edit under lib/ silently runs the previously compiled snapshot — same build, same error, byte for byte, reading as a wrong fix rather than one that never executed. I first fixed this in tool_revision and then measured it, which argued against shipping it: detecting a dirty tree costs ~28 ms of git status on every invocation, about 6% of a 493 ms startup, paid by every user of a public CLI to help only the few editing it. Happy to split either docs change out.

`-allowProvisioningUpdates` lets Xcode create or refresh a provisioning
profile, but on its own it can only do that through a signed-in Xcode
account. On a machine without a usable one -- CI, or a developer who
only has an API key -- xcodebuild cannot fetch the profile and quietly
settles for a cached wildcard instead.

Any app with an entitlement then fails, and fails misleadingly:

  error: Provisioning profile "tvOS Team Provisioning Profile: *"
  doesn't include the Game Center capability.

That names the capability the wildcard profile lacks, not the credential
that is actually missing, so the obvious next step is to go checking the
App ID's capabilities in the developer portal -- where everything is
already correct. The App ID in the case that prompted this had Game
Center enabled all along; what it had no tvOS development profile.

xcodebuild accepts an API key as the alternative, so forward one when
the environment provides all three of:

  APP_STORE_CONNECT_KEY_PATH   the .p8 private key
  APP_STORE_CONNECT_KEY_ID     its key id
  APP_STORE_CONNECT_ISSUER_ID  the issuer id

Absent or incomplete, nothing is added and a machine with a working
Xcode account behaves exactly as before. Set but pointing at a file that
does not exist earns a warning rather than a silent fallback, because
staying quiet there is indistinguishable from never having configured it
-- and the build goes on to fail with the capability message above,
several minutes later and nowhere near the cause.

Only the key id is ever logged; the key's contents are read by
xcodebuild, never by this process.

`resolveAuthenticationArgs` is top-level and takes its environment, file
system and logger as arguments so the tests can drive every branch
directly rather than restating the logic.
@MAUstaoglu
MAUstaoglu force-pushed the fix/xcodebuild-asc-api-key branch from e4b1758 to 4066b27 Compare September 3, 2026 08:24
@MAUstaoglu MAUstaoglu changed the title Let xcodebuild authenticate with an App Store Connect API key (1.10.0) 1.10.0: App Store Connect API key auth, and a snapshot that sees your edits Sep 3, 2026
`shared.sh` only recompiles bin/cache/flutter-tvos.snapshot when the git
revision changes, so editing lib/ and running the CLI silently executes
the previously compiled snapshot. The change does not look ignored, it
looks wrong: same build, same error, byte for byte.

Fixing it in the tool was the first instinct, and measuring talked me
out of it. Detecting a dirty tree costs ~28ms of git status on every
invocation -- about 6% of a 493ms startup -- paid by every user of a
public CLI to help only the few people editing it. A note costs nothing
and reaches the same people.
@MAUstaoglu
MAUstaoglu force-pushed the fix/xcodebuild-asc-api-key branch from 4066b27 to 83db85a Compare September 3, 2026 08:29
@MAUstaoglu MAUstaoglu changed the title 1.10.0: App Store Connect API key auth, and a snapshot that sees your edits Let xcodebuild authenticate with an App Store Connect API key (1.10.0) Sep 3, 2026
@MAUstaoglu
MAUstaoglu force-pushed the fix/xcodebuild-asc-api-key branch from 83db85a to c799086 Compare September 3, 2026 08:32
@MAUstaoglu MAUstaoglu changed the title Let xcodebuild authenticate with an App Store Connect API key (1.10.0) Let xcodebuild authenticate with an App Store Connect API key Sep 3, 2026
@MAUstaoglu
MAUstaoglu changed the base branch from main to dev September 3, 2026 08:32
Minor rather than patch: the API-key path is a new capability, and it is
additive -- absent the three environment variables nothing changes for a
machine with a working Xcode account.

Note that CONTRIBUTING.md asks contributors to leave the version alone
and file under [Unreleased], because maintainers assign numbers when
cutting a release. This is that call being made deliberately rather than
the convention being missed.
@MAUstaoglu MAUstaoglu changed the title Let xcodebuild authenticate with an App Store Connect API key Let xcodebuild authenticate with an App Store Connect API key (1.10.0) Sep 3, 2026
@MAUstaoglu
MAUstaoglu requested a review from DenisovAV September 3, 2026 12:27

@DenisovAV DenisovAV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The premise checks out — -authenticationKeyPath / -authenticationKeyID / -authenticationKeyIssuerID are exact per man xcodebuild, all-three-or-nothing matches Apple's documented requirement, and the argument only reaching device builds is right. What follows is about the paths where the feature is configured but doesn't take.

The stated principle isn't implemented for the case CI actually hits

application.dart:447-453 returns const <String>[] with no output at any verbosity whenever the trio is incomplete or any value is empty. The missing-file branch got a warning on the reasoning that "silently falling back looks identical to never having configured it" — partial configuration is that same failure with a likelier cause, and gets nothing.

In GitHub Actions an undefined secret interpolates to the empty string, not to an absent variable:

APP_STORE_CONNECT_KEY_ID: ${{ secrets.ASC_KEY_ID }}   # secret missing → ""

All three variables are set. The operator has demonstrably configured the feature. The gate treats it as unconfigured, says nothing, and the build dies minutes later with the misleading capability error this PR exists to eliminate — with nothing in the log connecting the two.

The gate is correct to refuse a partial flag set; the defect is that it is mute. Presence in the environment map separates "configured something" from "configured nothing" at no cost.

Two things make this more likely than it looks:

  • The variable names are asymmetric. APP_STORE_CONNECT_KEY_PATH, APP_STORE_CONNECT_KEY_ID, then APP_STORE_CONNECT_ISSUER_ID, dropping the KEY_. The flag it maps to is -authenticationKeyIssuerID, so APP_STORE_CONNECT_KEY_ISSUER_ID is the natural guess, and it is silently ignored.
  • README.md documents none of the three. They appear only in CHANGELOG.md and the dartdoc, so a user is guessing from the flag name. The README diff here is the version bump alone.

The success path is equally silent: printTrace at :465 is a literal no-op in StdoutLogger (logger.dart:563, class opens at :427), so at default verbosity "working", "half-configured" and "never configured" are indistinguishable from the log.

A literal ~ is never expanded, which breaks the documented CI form

The CHANGELOG and dartdoc both show:

export APP_STORE_CONNECT_KEY_PATH=~/.appstoreconnect/private_keys/AuthKey_XXXX.p8

Unquoted in an interactive shell that works, because the shell expands it first. A CI env: block, a quoted assignment, a .env file and an Xcode scheme variable all deliver a literal ~:

absolute('~/.appstoreconnect/…/AuthKey_ABC.p8')  →  /~/.appstoreconnect/…/AuthKey_ABC.p8
existsSync()                                     →  false

The warning fires, but reads "…does not exist" and never names the tilde as the cause. Same line, same class: values are not trimmed, so KEY_ID=$(cat keyid) or any secret store that appends a newline forwards it verbatim — and a trailing \n in a path renders a warning whose path looks correct because the newline is invisible.

The warning lands where this file says warnings are useless

resolveAuthenticationArgs is called during construction of the argument list at :615-620, inside the try that follows startProgress('Running Xcode build...') at :588. It renders — printWarning pauses the spinner — but at the top of a multi-minute build, and if the build then fails, :628-630 dump the whole of xcodebuild's stdout and stderr on top of it.

:635-639 states the rule this violates, about its own migration guards: emitted after the build "so they are the last thing on screen — pod install and the multi-minute Xcode spinner would otherwise scroll an earlier warning out of view." Hoisting the call next to _resolveSigningArgs at :586 puts it above the spinner.

"Only the key id is ever logged" is false two ways

The claim is in the dartdoc (:436-437) and the CHANGELOG.

  • :459-462 logs the full key path, and tvos_code_signing_test.dart:226 deliberately pins it there.
  • xcodebuild echoes its own command line into stdout, and :629 pipes that to printError on failure — so path, key id and issuer id all land in visible output on every failed device build.

The .p8 contents genuinely never pass through the CLI, so nothing secret leaks. It is an explicit security claim that doesn't hold as written.

The tests don't hold what the description says they hold

resolveAuthenticationArgs is top-level and takes its environment, file system and logger as arguments, so the tests drive every branch directly

Four of eight decision points are unpinned. Mutating the source and re-running:

mutation result
delete the spread at the xcodebuild call site survives
drop the !buildInfo.simulator guard at the call site survives
drop the .isEmpty guards survives
drop fileSystem.path.absolute(keyPath) survives
delete the existsSync() block fails
swap the key id and issuer id values fails
printWarningprintTrace fails

The first is the one that matters: the feature can be disconnected from the build entirely and the suite stays green.

treats an empty value as unset passes for the wrong reason — it only empties path, and an empty path falls through to existsSync(''), which is also false. Remove the .isEmpty guards and it still returns [], now with a garbage warning reading "…but does not exist". Empty KEY_ID and ISSUER_ID are never tested, and empty-not-null is the realistic CI shape of an unset secret.

path.absolute() is load-bearing rather than cosmetic: existsSync() resolves against the CLI's cwd while xcodebuild runs with workingDirectory: tvosProjectDir.path, so without it a relative key path passes the existence check and is then handed to xcodebuild one directory deeper. Every test passes an already-absolute path, so the line is invisible to them.

The new function took NativeTvosBundle's documentation

The function was inserted between the class's doc comment and the class, with no blank line between :416 (/// 6. Invoke xcodebuild targeting appletvos or appletvsimulator SDK) and :417. The merged block now documents resolveAuthenticationArgs, whose summary sentence reads "Orchestrates the native tvOS build via xcodebuild" followed by the six-step build pipeline, and NativeTvosBundle at :476 has no doc comment at all.

Two notes on files this PR already touches

CONTRIBUTING.md:47 says "There are 74 unit tests in test/general/" — there are 436, across 40 files.

The new snapshot section is right about the mechanism (shared.sh:32-46: a git checkout keys off rev-parse HEAD, a non-git install shasums bin and lib), but "an edit under lib/ or bin/ therefore does nothing until you remove it" holds only for uncommitted edits — committing moves HEAD and rebuilds.

Unrelated to this change, but in the group your new tests sit beside: Code signing - simulator vs device asserts expect(const <String>[], isEmpty) and expect(isSimulator, isFalse) against local constants. Neither test can fail. This PR is the first thing in that file to import production code at all.

The gate was right to refuse a partial flag set -- xcodebuild rejects the
key arguments unless all three are present -- but it returned an empty
list with no output at any verbosity, which is the exact failure this
feature exists to prevent. An undefined GitHub Actions secret interpolates
to the empty string rather than to an absent variable, so all three
variables are set, the operator has demonstrably configured this, and the
build still died minutes later with the misleading capability error and
nothing in the log connecting the two.

Presence in the environment map separates "configured nothing" from
"configured something broken" at no cost, and the warning names the
missing variable -- worth doing because the names are asymmetric
(_KEY_PATH, _KEY_ID, then _ISSUER_ID), so APP_STORE_CONNECT_KEY_ISSUER_ID
is the natural guess and was silently ignored.

Also on that path:

- Expand a leading `~`. An interactive shell expands it before the CLI
  sees it, but a CI env: block, a quoted assignment, a .env file and an
  Xcode scheme variable all deliver it literally, where absolute() turned
  it into /~/... and existsSync failed with a warning that never named
  the tilde as the cause.
- Trim values. `KEY_ID=$(cat keyid)` and most secret stores append a
  newline; in a path the invisible \n renders a warning whose path looks
  perfectly correct.
- printStatus rather than printTrace on success. StdoutLogger.printTrace
  is a literal no-op, so at default verbosity "working", "half-configured"
  and "never configured" were indistinguishable from the log.
- Resolve the args before startProgress. The warning rendered, but at the
  top of a multi-minute build and under xcodebuild's full stdout/stderr
  if the build then failed -- the same reason the migration guards below
  are deliberately emitted after the build.

Extract the xcodebuild argv into NativeTvosBundle.xcodebuildArgs, matching
aotAssembleArgs and aotLinkArgs. Four decision points were unpinned
before: the feature could be unhooked from the build entirely, the
simulator guard dropped, the .isEmpty guards dropped and absolute()
dropped, all with the suite still green. `treats an empty value as unset`
passed for the wrong reason -- it only emptied the path, which then fell
through to existsSync(''), also false.

Tests: 5 -> 17, and all eight mutations in that table now fail. Drops the
two cases in `Code signing - simulator vs device`, which asserted against
local constants and could not fail; the new wiring group makes the same
two claims against production code.

Full suite 388 passing / 58 failing, against the same 58 on dev.
README documented none of the three variables -- they appeared only in
CHANGELOG.md and the dartdoc, so a user was guessing them from the
xcodebuild flag names, and guessing wrong on the issuer one. Adds them to
the Code signing section, where the engine-signing variables already live,
with the failure they fix and a note that the third has no KEY_.

"Only the key id is ever logged" was false two ways: the warning path
logs the full key path, and xcodebuild echoes its own command line into
stdout, which the caller dumps to printError on every failed device
build -- so path, key id and issuer id all reach visible output. Nothing
secret leaks, because the .p8 contents genuinely never pass through the
CLI, but it was an explicit security claim that did not hold as written.
States what is actually true instead, in both the CHANGELOG and the
dartdoc.
"There are 74 unit tests in test/general/" -- there are roughly 450,
across 40 files. Gives an approximate figure rather than an exact one,
which is what went stale, and adds the baseline a contributor actually
needs: the suite is not green, dev reports 58 failures at HEAD, so
compare against that rather than against zero.

The snapshot section was right about the mechanism but overstated it:
shared.sh keys off `git rev-parse HEAD`, so committing an edit moves HEAD
and rebuilds. It is *uncommitted* edits that are silently ignored, which
is the edit-and-try loop specifically.
@MAUstaoglu

Copy link
Copy Markdown
Member Author

All seven addressed, in three commits.

The mute partial-configuration path was the real defect — the gate now tests presence in the environment map, so an empty-string CI secret is recognised as configured and warned about, naming the missing variable and the KEY_ISSUER_ID guess. ~ expands, values are trimmed, success is printStatus (you're right that printTrace is a no-op in StdoutLogger), and the resolver runs before startProgress.

The security claim was wrong both ways you describe; the dartdoc and CHANGELOG now say the .p8 contents never pass through the CLI but the path, key id and issuer id do appear in output. NativeTvosBundle has its dartdoc back.

On the tests — fair hit. Extracted NativeTvosBundle.xcodebuildArgs, following aotAssembleArgs/aotLinkArgs, so the wiring is testable. Your mutation table now fails on all four survivors, including deleting the spread at the call site. 5 → 17 tests. I also dropped the two simulator vs device cases that couldn't fail; the new group makes both claims against production code — happy to restore them separately if you'd rather.

CONTRIBUTING: count corrected (approximate, since exactness is what went stale) plus the 58-failure baseline, and the snapshot note now says uncommitted edits.

388 passing / 58 failing, against the same 58 on dev. Version left at 1.10.0 per your earlier call — say if that's moved.

@MAUstaoglu
MAUstaoglu requested a review from DenisovAV September 4, 2026 09:09
The previous commit asserted the suite is not green and that dev reports
58 failures at HEAD. That is wrong, and the number was an artifact of how
I ran it. Both dev and this branch are green: 431 and 446 passing, zero
failing, which is exactly the +15 net tests this branch adds.

The 58 came from running `dart test` without the resolved TMPDIR that CI
passes. Flutter's test harness installs an FS guard that resolves
symlinks when computing the allowed temp root but not on the path it
checks; $TMPDIR is /var/folders/... and /var is a symlink to
/private/var, so the two never compare equal.

The root cause is in this file: the documented command omitted the
prefix, so anyone following CONTRIBUTING hits ~58 phantom failures that
look like real breakage. test/README.md has carried the explanation and
the correct invocation all along, and CI uses it -- CONTRIBUTING just
never picked it up. Adds the prefix to the command, states the suite is
green, and explains the trap rather than baking a bogus baseline into
the docs.
@MAUstaoglu

Copy link
Copy Markdown
Member Author

Correction to my previous comment: the 58-failure baseline is wrong, and so was the same claim in the PR description. Both dev and this branch are green.

  • dev (cc9caf5): 431 passing, 0 failing
  • this branch (8ad2129): 446 passing, 0 failing — the +15 being this PR's net test change

The 58 came from running dart test without the resolved TMPDIR that CI passes. The FS guard resolves symlinks when computing the allowed temp root but not on the path it checks, and on macOS $TMPDIR is /var/folders/… against /var/private/var.

The reason I hit it is in CONTRIBUTING.md itself: the documented command omits the prefix, so anyone following it sees ~58 phantom failures that read as real breakage. test/README.md has had the explanation and the correct invocation all along. Pushed 8ad2129, which adds the prefix to the documented command, states the suite is green, and drops the bogus baseline I had added a commit earlier.

Apologies for putting a made-up number in front of you twice — the original "373 → 378, same 58 on dev" in the description was the same artifact.

1.9.0 stopped publishing origin-signed artifacts and signs the engine locally
instead, but it signed with `Developer ID Application`. Only a team's Account
Holder can create one, there is a per-team cap, and a tvOS developer has no
other reason to hold one -- it is a macOS outside-the-App-Store certificate.
A developer without one got a warning on an otherwise successful build and an
unsigned engine in the bundle, then ITMS-91065 at submission.

That is a regression against 1.4.0-1.8.0, where the published artifact carried
the maintainer's signature and nothing was asked of the developer. It reached
a customer, who upgraded and was rejected on a submission that worked before.

`Apple Distribution` is the certificate every developer who can upload to
TestFlight already has, so signing now asks nothing extra of anyone.

Confirmed against Apple rather than reasoned about: Crown Breaker 1.2.1 build
23, engine signed `Apple Distribution` before embedding, processed to VALID
and cleared external testing. The same app's unsigned builds (16, 17, 19, 20)
are INVALID and its Developer-ID builds (21, 22) are VALID.

Developer ID also satisfies the check and is what flutter.dev signs its own
engine with, but it is deliberately not accepted: nobody shipping a tvOS app
needs one, so supporting it would only add a branch that almost never fires.
A development certificate is still refused -- it is not known to satisfy the
check, and silently using one would hide the real problem.

Restoring the old certificate type fails the suite, so the gap cannot reopen
silently.
@MAUstaoglu
MAUstaoglu force-pushed the fix/xcodebuild-asc-api-key branch from 26eea54 to 3d01f80 Compare September 4, 2026 11:03

@DenisovAV DenisovAV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified against 3d01f80 by mutating the source and re-running, not from the description.

The API-key work

All four survivors from the previous round are dead, and the branches the fix introduced are pinned too:

mutation result
drop authenticationArgs from the xcodebuild argv fails
drop the id/issuer emptiness guards fails (−4)
drop fileSystem.path.absolute() fails
drop the .trim() on all three values fails
never expand a leading ~ fails
warn when nothing is configured fails (−2)
never warn on partial configuration fails (−3)
printStatus back to printTrace fails
drop the simulator guard on resolution survives

The survivor is benign for the argv — xcodebuildArgs carries its own if (!isSimulator) ...authenticationArgs, and that guard is pinned by the first mutation, so the flags still cannot reach a simulator build. What is unpinned is the diagnostics: with the outer guard gone, a simulator build on a machine with the three variables exported would emit the printStatus line, or a provisioning-fallback warning, for a build that is never signed. Worth a test only if you want the messaging pinned; correctness is covered.

Resolving before existsSync rather than after is a better fix than the one I asked for. Previously the check ran against the raw value while the flag carried the absolutised one; now both use resolved, so what is checked and what xcodebuild receives cannot diverge — that removes the relative-path discrepancy at the source instead of compensating for it. _expandTilde taking environment rather than reading HOME from the process keeps the no-HOME branch testable, and passing the ~ through with a warning that says so is the right fallback.

The suite is green at 447 passing, 0 failing with the documented invocation, and CONTRIBUTING.md now carries the resolved-TMPDIR prefix that test/README.md had all along. That is worth more than the count it replaced: the previous line sent anyone following it into ~58 phantom failures that read as real breakage.

The engine-signing commit

The regression is real and the evidence is sound. Restricting to Developer ID Application is a genuine break from 1.4.0–1.8.0: only a team's Account Holder can create one, there is a per-team cap, and a tvOS developer has no other reason to hold a macOS outside-the-App-Store certificate — so the practical outcome was an unsigned engine and ITMS-91065 on submissions that passed before the upgrade. The Crown Breaker builds separate the variables properly: 23 VALID and cleared external review with the engine signed Apple Distribution, 16/17/19/20 INVALID unsigned, 21/22 VALID with Developer ID.

One thing to confirm you meant, because it changed between revisions. The earlier version accepted Apple Distribution in addition to Developer ID Application; this one accepts it instead. That turns a purely additive fix into a substitution, and it narrows what 1.9.0 required: a developer who created a Developer ID because 1.9.0's warning told them to, and who has no Apple Distribution, goes from signed to unsigned. In practice that set is close to empty — nobody uploads to App Store Connect without a distribution certificate — but the CHANGELOG's "No action is needed on an existing project" is written for the additive version, and it is the one line a reader in that position would rely on. Either widen it back or say plainly that Developer ID is no longer used.

Two comments did not follow the code:

  • tvos_engine_signing.dart:30 — "Signs engine artifacts with a Developer ID from the local keychain."
  • :247 — "a developer without a Developer ID can still build and run locally"

Mutations: reverting kIdentityPrefix to Developer ID Application: fails (−5), and adding Apple Development: to the match fails (−7), so the 1.9.0 behaviour cannot return silently and the refusal of development certificates is pinned. Widening the match to accept Developer ID Application as well survives — harmless, since both satisfy the check, but "deliberately the only accepted type" is a claim nothing holds.

Scope

This is a customer-facing regression fix for a submission path that is broken today, sitting in a PR titled for API-key authentication that has now been through two review rounds. Bundling it means the fix waits on the feature, the 1.10.0 entry describes two unrelated changes under one heading, and the regression is invisible to anyone reading release notes for the signing behaviour that bit them. Cherry-picking it onto its own PR against dev costs nothing and can land immediately.

Approving on the API-key work. Whether the signing fix rides along is your call — it should not be what delays it.

@MAUstaoglu
MAUstaoglu merged commit fb41c58 into fluttertv:dev Sep 4, 2026
2 checks passed
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.

2 participants