Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ apps/swift/xcconfig/*.local.xcconfig
apps/swift/PackRat.xcodeproj/
apps/swift/*.xcworkspace/xcuserdata/
apps/swift/DerivedData*/
apps/swift/build/
apps/swift/build.log
apps/swift/upload.log
# Signing material — never commit private keys / certs (fastlane cert output)
apps/swift/*.p12
apps/swift/*.cer
apps/swift/*.certSigningRequest
apps/swift/.build/
apps/swift/.swiftpm/
apps/swift/Package.resolved
Expand Down
11 changes: 9 additions & 2 deletions apps/swift/Resources/Info-iOS.plist
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>GOOGLE_IOS_CLIENT_ID</key>
<string>993694750638-97t0vhfml04u2avrlbve22jbs9qcinbc.apps.googleusercontent.com</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
Expand All @@ -74,5 +74,12 @@
<key>UIColorName</key>
<string></string>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
2 changes: 1 addition & 1 deletion apps/swift/Resources/Info-watchOS.plist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
<key>WKApplication</key>
<true/>
<key>WKCompanionAppBundleIdentifier</key>
<string>com.andrewbierman.packrat</string>
<string>com.andrewbierman.packrat.swift</string>
</dict>
</plist>
10 changes: 4 additions & 6 deletions apps/swift/Sources/PackRat/PackRatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ struct PackRatApp: App {
#endif

init() {
// Telemetry has to start before any view is mounted so launch-time
// errors are captured. A missing DSN silently disables the SDK.
SentryConfig.start()

#if os(macOS)
if ProcessInfo.processInfo.arguments.contains("--reset-auth") {
UserDefaults.standard.set(true, forKey: "ApplePersistenceIgnoreState")
Expand All @@ -20,12 +24,6 @@ struct PackRatApp: App {
#endif
}

init() {
// Telemetry has to start before any view is mounted so launch-time
// errors are captured. A missing DSN silently disables the SDK.
SentryConfig.start()
}

var body: some Scene {
WindowGroup {
AuthGateView()
Expand Down
23 changes: 16 additions & 7 deletions apps/swift/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ targets:
sources:
- Sources/PackRat
- Sources/PackRatShared
resources:
- Resources/Assets.xcassets
entitlements:
path: Resources/PackRat-iOS.entitlements
Expand All @@ -71,6 +70,13 @@ targets:
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
LSRequiresIPhoneOS: true
# All four orientations are required for iPad (device family "1,2");
# App Store validation (error 90474) rejects the bundle otherwise.
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UILaunchScreen:
UIColorName: ""
UIApplicationSceneManifest:
Expand Down Expand Up @@ -117,17 +123,20 @@ targets:
product: Sentry
- package: GoogleSignIn
product: GoogleSignIn
- target: PackRat-Watch
# PackRat-Watch is intentionally NOT embedded in distribution builds yet:
# the watch app has no app icons, which App Store validation rejects
# (error 90391). Re-add this dependency once watch icons exist.
settings:
base:
SWIFT_VERSION: "5.9"
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: 7WV9JYCW55
PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat
DEVELOPMENT_TEAM: 666HGMV2LU
PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.swift
PRODUCT_MODULE_NAME: PackRat
TARGETED_DEVICE_FAMILY: "1,2"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon

PackRat-Watch:
type: application
Expand All @@ -143,16 +152,16 @@ targets:
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
WKApplication: true
WKCompanionAppBundleIdentifier: com.andrewbierman.packrat
WKCompanionAppBundleIdentifier: com.andrewbierman.packrat.swift
ITSAppUsesNonExemptEncryption: false
settings:
base:
SWIFT_VERSION: "5.9"
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: 7WV9JYCW55
PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.watchkitapp
DEVELOPMENT_TEAM: 666HGMV2LU
PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.swift.watchkitapp
PRODUCT_MODULE_NAME: PackRatWatch

PackRat-macOS:
Expand Down
126 changes: 126 additions & 0 deletions apps/swift/scripts/upload-testflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bun
/**
* Archive the native Swift PackRat iOS app and upload it to TestFlight.
*
* This targets a SEPARATE App Store Connect record from the production Expo
* app: bundle id `com.andrewbierman.packrat.swift`. Register that app record
* in App Store Connect once before the first upload.
*
* Auth uses an Apple ID + app-specific password (no App Store Connect API key
* required). Generate a password at appleid.apple.com -> Sign-In & Security ->
* App-Specific Passwords.
*
* Required env (put in apps/swift/.env.local, gitignored):
* APPLE_ID your Apple ID email
* APPLE_APP_PASSWORD app-specific password (xxxx-xxxx-xxxx-xxxx)
* APPLE_TEAM_ID the team that owns the record (e.g. 7WV9JYCW55)
*
* Optional env:
* BUILD_NUMBER CFBundleVersion for this upload (default: timestamp)
*
* Usage:
* bun apps/swift/scripts/upload-testflight.ts
*/
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const SWIFT_DIR = new URL('..', import.meta.url).pathname;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use import.meta.dir to prevent path resolution failures.

new URL(...).pathname returns a URL-encoded string. If the repository's path contains spaces, .pathname will contain %20 instead of literal spaces, causing downstream xcodebuild steps to fail because the .xcodeproj file cannot be found. Since this is a Bun script, you can use the built-in import.meta.dir.

🛠 Proposed fix
-const SWIFT_DIR = new URL('..', import.meta.url).pathname;
+const SWIFT_DIR = join(import.meta.dir, '..');
📝 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
const SWIFT_DIR = new URL('..', import.meta.url).pathname;
const SWIFT_DIR = join(import.meta.dir, '..');
🤖 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 `@apps/swift/scripts/upload-testflight.ts` at line 29, Update the SWIFT_DIR
definition to use Bun’s import.meta.dir instead of constructing a URL pathname,
ensuring paths containing spaces remain correctly decoded for downstream
xcodebuild operations.

const PROJECT = join(SWIFT_DIR, 'PackRat.xcodeproj');
const SCHEME = 'PackRat-iOS';
const BUNDLE_ID = 'com.andrewbierman.packrat.swift';

function req(name: string): string {
const v = process.env[name];
if (!v) {
console.error(`Missing required env var: ${name}. See script header.`);
process.exit(1);
}
return v;
}

const appleId = req('APPLE_ID');
const appPassword = req('APPLE_APP_PASSWORD');
const teamId = req('APPLE_TEAM_ID');
const buildNumber = process.env.BUILD_NUMBER ?? String(Math.floor(Date.now() / 1000));

const work = mkdtempSync(join(tmpdir(), 'packrat-tf-'));
const archivePath = join(work, 'PackRat.xcarchive');
const exportDir = join(work, 'export');

function run(cmd: string, args: string[]) {
console.log(`\n$ ${cmd} ${args.join(' ')}`);
Comment thread
mikib0 marked this conversation as resolved.
Dismissed
execFileSync(cmd, args, { stdio: 'inherit' });
}
Comment on lines +52 to +55

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.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Fix pipeline failure and prevent credential leak.

The custom linter failed because the run function has two parameters. Additionally, logging args.join(' ') leaks the Apple app-specific password in plain text during the TestFlight upload step.

Refactor run to take a single tuple parameter and redact the password in the logs. You will also need to update the three call sites to pass a single array.

🔒️ Proposed fixes for the function and call sites

1. Update the function definition:

-function run(cmd: string, args: string[]) {
-  console.log(`\n$ ${cmd} ${args.join(' ')}`);
-  execFileSync(cmd, args, { stdio: 'inherit' });
+function run([cmd, ...args]: [string, ...string[]]) {
+  const safeArgs = args.map((arg, i) => args[i - 1] === '--password' ? '***' : arg);
+  console.log(`\n$ ${cmd} ${safeArgs.join(' ')}`);
+  execFileSync(cmd, args, { stdio: 'inherit' });
 }

2. Update the three call sites:

 // 1. Archive for a real device (TestFlight cannot accept a simulator build).
-run('xcodebuild', [
+run([
+  'xcodebuild',
   'archive',
 // 2. Export a signed .ipa for App Store distribution.
 const exportOptions = join(work, 'ExportOptions.plist');
 ...
-run('xcodebuild', [
+run([
+  'xcodebuild',
   '-exportArchive',
 // 3. Upload to TestFlight via altool (app-specific-password auth).
 const ipa = join(exportDir, 'PackRat-iOS.ipa');
-run('xcrun', [
+run([
+  'xcrun',
   'altool',
📝 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
function run(cmd: string, args: string[]) {
console.log(`\n$ ${cmd} ${args.join(' ')}`);
execFileSync(cmd, args, { stdio: 'inherit' });
}
function run([cmd, ...args]: [string, ...string[]]) {
const safeArgs = args.map((arg, i) => args[i - 1] === '--password' ? '***' : arg);
console.log(`\n$ ${cmd} ${safeArgs.join(' ')}`);
execFileSync(cmd, args, { stdio: 'inherit' });
}
🧰 Tools
🪛 GitHub Actions: Checks / 0_checks.txt

[error] 52-52: Owned functions with too many params found (1). run has 2 params

🪛 GitHub Actions: Checks / checks

[error] 52-52: lint:custom failed: Owned functions with too many params found (1). Function 'run' has 2 params.

🤖 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 `@apps/swift/scripts/upload-testflight.ts` around lines 52 - 55, Refactor run
to accept one tuple containing the command and arguments, while preserving
execFileSync behavior. Redact the Apple app-specific password from the command
string before console.log output, and update all three run call sites to pass
their command and arguments as a single array.

Source: Pipeline failures


// 1. Archive for a real device (TestFlight cannot accept a simulator build).
run('xcodebuild', [
'archive',
'-project',
PROJECT,
'-scheme',
SCHEME,
'-destination',
'generic/platform=iOS',
'-archivePath',
archivePath,
// Lets Xcode register the App IDs and generate provisioning profiles for
// the (new) bundle ids on the fly, using the signed-in account.
'-allowProvisioningUpdates',
`CURRENT_PROJECT_VERSION=${buildNumber}`,
`DEVELOPMENT_TEAM=${teamId}`,
]);

// 2. Export a signed .ipa for App Store distribution.
const exportOptions = join(work, 'ExportOptions.plist');
writeFileSync(
exportOptions,
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store-connect</string>
<key>teamID</key><string>${teamId}</string>
<key>destination</key><string>export</string>
<key>signingStyle</key><string>automatic</string>
<key>uploadSymbols</key><true/>
</dict>
</plist>
`,
);

run('xcodebuild', [
'-exportArchive',
'-archivePath',
archivePath,
'-exportPath',
exportDir,
'-exportOptionsPlist',
exportOptions,
// Export also needs to generate the App Store distribution profiles for the
// new bundle ids on the fly.
'-allowProvisioningUpdates',
]);

// 3. Upload to TestFlight via altool (app-specific-password auth).
// `--asc-provider` (team short name) is required when the Apple ID belongs to
// more than one team, so altool knows which one to deliver to.
const ipa = join(exportDir, 'PackRat-iOS.ipa');
run('xcrun', [
'altool',
'--upload-app',
'--type',
'ios',
'--file',
ipa,
'--username',
appleId,
'--password',
appPassword,
'--asc-provider',
teamId,
]);

console.log(`\n✓ Uploaded build ${buildNumber} to TestFlight (${BUNDLE_ID}).`);
console.log('It will appear in App Store Connect after processing (usually 5-15 min).');
Loading