From 96ad180f81ab1fcd214571855cf53031a8e9c9db Mon Sep 17 00:00:00 2001 From: Gabriel Taveira Date: Mon, 27 Jul 2026 20:20:26 -0300 Subject: [PATCH 1/2] fix(mobile): give the widget and notification-service targets a team id @bacons/apple-targets reads ios.appleTeamId to stamp DEVELOPMENT_TEAM onto the targets it generates. The field was never set, so prebuild took the "no team anywhere" branch: it strips DEVELOPMENT_TEAM from every target and writes the literal `DevelopmentTeam = undefined;` into TargetAttributes. It also warns "iOS builds may fail until this is corrected" on every CI prebuild. Sourced from EXPO_APPLE_TEAM_ID so the id stays out of the repo. Also corrects release-mobile.yml's CREDENTIALS block, which predicted the wrong failure. EAS sets iOS credentials up per target, and in non-interactive mode SetUpDistributionCertificate can only reuse a certificate already assigned to that target: no branch adopts an unassigned account certificate and none creates one. An ASC API key does not help, since the rc1 run was already using one when it failed at the certificate lookup, and it cannot link App Group identifiers at all. The build step now names the target and the command to run instead of leaving eas-cli's bare "run this command again in interactive mode". Refs #104 Claude-Session: https://claude.ai/code/session_01QsVEsfkynXpJWK4t33exG9 --- .github/workflows/release-mobile.yml | 98 ++++++++++++++++--- apps/mobile/app.config.ts | 22 ++++- .../expo-target.config.js | 6 +- .../pegada-widgets/expo-target.config.js | 9 +- 4 files changed, 117 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 71b94a2..0c0064f 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -21,16 +21,40 @@ name: Release Mobile (release + build + submit) # CREDENTIALS # --------------------------------------------------------------------------- # iOS signing uses EAS's remote credentials (managed via `eas credentials`), -# fetched non-interactively over the same EXPO_TOKEN auth. In non-interactive -# mode, EAS CLI only allows REUSING an existing valid provisioning -# profile/certificate; it refuses to GENERATE a new one without an App Store -# Connect API key (EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / -# EXPO_ASC_ISSUER_ID), see: -# https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team -# If the stored credentials are valid this is a non-issue. If EAS decides it -# needs to mint a new profile mid-build, the iOS job will fail with an -# InsufficientAuthenticationNonInteractiveError, which needs an ASC API key -# from Gabriel to fix (only he can create one in App Store Connect). +# fetched non-interactively over the same EXPO_TOKEN auth. EAS sets them up +# per TARGET, not per app: the main app plus every extension listed under +# `extra.eas.build.experimental.ios.appExtensions`, which +# @bacons/apple-targets populates from apps/mobile/targets/*. Today: +# +# Pegada app.pegada +# PegadaWidgets app.pegada.widgets +# PegadaNotificationService app.pegada.notificationservice +# +# In non-interactive mode eas-cli will only REUSE a distribution certificate +# that is already assigned to that specific target. SetUpDistributionCertificate +# has no branch that adopts an unassigned account certificate and none that +# creates one; it throws MissingCredentialsNonInteractiveError, printed as +# "Credentials are not set up. Run this command again in interactive mode." +# A target EAS has never seen therefore cannot be built from CI. That is what +# killed v1.5.0-rc1's iOS job seconds after it started (#104), on the first +# tag since the two extensions landed. +# +# An ASC API key (EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / EXPO_ASC_ISSUER_ID) +# does not substitute for that setup. rc1 was already authenticated with one +# ("Using App Store Connect API Key from EAS credentials service") and still +# failed at the certificate lookup, which happens before any profile work. The +# key also cannot link App Group identifiers: eas-cli's +# syncCapabilityIdentifiersForEntitlementsAsync bails out with "Skipping +# capability identifier syncing because the current Apple authentication +# session is not using Cookies", and app.pegada.widgets needs group.app.pegada +# linked to its App ID. +# +# So each new extension target costs one `eas credentials -p ios` run from +# apps/mobile against the production profile, signed in with the Apple ID +# (cookie auth, which is what syncs the App Group), reusing the existing +# distribution certificate. EAS mints and stores that target's provisioning +# profile once, and every later non-interactive build reuses it. Only Gabriel +# can do this; it needs App Store Connect access. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- @@ -325,15 +349,65 @@ jobs: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} + # EXPO_APPLE_TEAM_ID backs `ios.appleTeamId` in app.config.ts, which is + # the only thing @bacons/apple-targets reads to stamp DEVELOPMENT_TEAM + # onto the generated extension targets. Env vars DO survive into + # `eas build --local`'s isolated working copy (files don't, see the + # GoogleService steps above), so setting it on this step is enough. + # + # The failure branch translates eas-cli's "Credentials are not set up" + # into the actual remediation. On its own that message names neither the + # target that is missing credentials nor the command that fixes it, and + # it arrives ~40s into a job the release notes have already shipped + # ahead of, so it is easy to misread as a generic auth problem (it was: + # #104's first diagnosis blamed the ASC API key, which is unrelated). - name: EAS build (iOS, production, local) working-directory: apps/mobile + env: + EXPO_APPLE_TEAM_ID: ${{ secrets.EXPO_APPLE_TEAM_ID }} run: | - eas build \ + set -uo pipefail + + if [ -z "${EXPO_APPLE_TEAM_ID:-}" ]; then + echo "::warning::EXPO_APPLE_TEAM_ID is not set. Prebuild will write \`DevelopmentTeam = undefined\` into the widget and notification-service targets." + fi + + if eas build \ --platform ios \ --profile production \ --local \ --non-interactive \ - --output "$RUNNER_TEMP/Pegada.ipa" + --output "$RUNNER_TEMP/Pegada.ipa" 2>&1 | tee "$RUNNER_TEMP/eas-ios.log"; then + exit 0 + fi + + if ! grep -q "Credentials are not set up" "$RUNNER_TEMP/eas-ios.log"; then + exit 1 + fi + + TARGET=$(grep -oE "Setting up credentials for target .*" "$RUNNER_TEMP/eas-ios.log" \ + | tail -1 \ + | sed 's/^Setting up credentials for target //') + + echo "::error::EAS has no stored iOS credentials for target ${TARGET:-unknown}. Run 'eas credentials -p ios' from apps/mobile, pick the production profile and that target, sign in with the Apple ID, and reuse the existing distribution certificate." + { + echo "### iOS credentials missing for \`${TARGET:-unknown}\`" + echo + echo "EAS stores signing credentials per target. This one has none, and" + echo "\`--non-interactive\` can only reuse credentials that already exist." + echo + echo "Fix it once, from \`apps/mobile\`:" + echo + echo '```' + echo "eas credentials -p ios" + echo '```' + echo + echo "Pick the \`production\` profile and that target, sign in with the Apple ID" + echo "(not an ASC API key, which cannot link App Group identifiers), and reuse" + echo "the existing distribution certificate. See the CREDENTIALS block at the top" + echo "of \`.github/workflows/release-mobile.yml\`." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 - name: Upload .ipa artifact uses: actions/upload-artifact@v7 diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 2be6f93..f0c0e7f 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -57,8 +57,10 @@ const config: ExpoConfig = { // `notification-service` extension that restyles chat pushes as iOS // communication notifications. iOS allows one widget extension per app, so // every widget-family feature registers in PegadaWidgetsBundle.swift - // instead of adding a target. Team ID comes from EAS credentials at build - // time; local sim builds don't sign. + // instead of adding a target. Both targets take their DEVELOPMENT_TEAM + // from `ios.appleTeamId` below, and each needs its own credentials set up + // on EAS before a release build can sign it (see the CREDENTIALS block in + // .github/workflows/release-mobile.yml). Local sim builds don't sign. "@bacons/apple-targets", "expo-secure-store", "expo-notifications", @@ -325,6 +327,22 @@ const config: ExpoConfig = { usesNonExemptEncryption: false, }, bundleIdentifier: "app.pegada", + // @bacons/apple-targets reads this to stamp DEVELOPMENT_TEAM onto every + // target it generates (see with-xcode-changes.js, + // applyDevelopmentTeamIdToTargets). It is NOT supplied by EAS credentials + // at build time -- prebuild runs long before the signing step, and when + // the field is absent the plugin takes its "no team anywhere" branch: + // it REMOVES DEVELOPMENT_TEAM from every target and writes the literal + // string `DevelopmentTeam = undefined;` into the project's + // TargetAttributes (reproduced locally with `expo prebuild -p ios`; it + // also warns "iOS builds may fail until this is corrected" on every + // single prebuild, including the ones in CI). + // + // Sourced from the environment rather than hardcoded so the team ID + // stays out of a public repo; release-mobile.yml passes it from the + // EXPO_APPLE_TEAM_ID secret. Unset (plain local `expo prebuild`, + // simulator builds) behaves exactly as before -- those don't sign. + appleTeamId: process.env.EXPO_APPLE_TEAM_ID, entitlements: { // Shared storage between the app and the widget extension: the matches // snapshot (UserDefaults) + downloaded avatars (container files). diff --git a/apps/mobile/targets/notification-service/expo-target.config.js b/apps/mobile/targets/notification-service/expo-target.config.js index 5d65321..1f1b88b 100644 --- a/apps/mobile/targets/notification-service/expo-target.config.js +++ b/apps/mobile/targets/notification-service/expo-target.config.js @@ -19,8 +19,10 @@ module.exports = { entitlements: { // Communication-notification styling. Must also be enabled on the App ID // in the Apple Developer portal for device builds (simulator doesn't - // enforce it). The main app carries the same entitlement via - // `ios.entitlements` in app.config.ts. + // enforce it), and this target needs its own credentials stored on EAS for + // app.pegada.notificationservice before a release build can sign it; see + // the CREDENTIALS block in .github/workflows/release-mobile.yml. The main + // app carries the same entitlement via `ios.entitlements` in app.config.ts. "com.apple.developer.usernotifications.communication": true, }, }; diff --git a/apps/mobile/targets/pegada-widgets/expo-target.config.js b/apps/mobile/targets/pegada-widgets/expo-target.config.js index 38804ea..b6c73bc 100644 --- a/apps/mobile/targets/pegada-widgets/expo-target.config.js +++ b/apps/mobile/targets/pegada-widgets/expo-target.config.js @@ -9,8 +9,13 @@ // deploymentTarget 16.2 is the floor for the ActivityKit content APIs the // Live Activity uses; newer-OS features (iOS 17 widget APIs, iOS 18 // controls) gate themselves with @available / #available in their own Swift -// files. Team ID comes from EAS credentials at release build time; local -// simulator builds don't sign. +// files. +// +// DEVELOPMENT_TEAM comes from `ios.appleTeamId` in app.config.ts, at prebuild +// time, not from EAS credentials. Signing this target in CI also needs its own +// credentials stored on EAS for app.pegada.widgets, plus group.app.pegada +// linked to that App ID; see the CREDENTIALS block in +// .github/workflows/release-mobile.yml. Local simulator builds don't sign. module.exports = { type: "widget", name: "PegadaWidgets", From c895140b46b704bbba186da8bfa3effdf02530e4 Mon Sep 17 00:00:00 2001 From: Gabriel Taveira Date: Mon, 27 Jul 2026 20:31:33 -0300 Subject: [PATCH 2/2] fix(mobile): keep the credentials annotation alive when the target line is absent pipefail plus the job shell's -e meant a no-match grep would kill the step before it printed the annotation it exists to print. Refs #104 Claude-Session: https://claude.ai/code/session_01QsVEsfkynXpJWK4t33exG9 --- .github/workflows/release-mobile.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 0c0064f..98b6638 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -385,9 +385,12 @@ jobs: exit 1 fi + # `|| true` because pipefail is on and the job's shell runs with -e: + # a no-match grep here would kill the step before it can print the + # annotation, which is the only reason we got this far. TARGET=$(grep -oE "Setting up credentials for target .*" "$RUNNER_TEMP/eas-ios.log" \ | tail -1 \ - | sed 's/^Setting up credentials for target //') + | sed 's/^Setting up credentials for target //' || true) echo "::error::EAS has no stored iOS credentials for target ${TARGET:-unknown}. Run 'eas credentials -p ios' from apps/mobile, pick the production profile and that target, sign in with the Apple ID, and reuse the existing distribution certificate." {