Compare commits

..

5 Commits

Author SHA1 Message Date
Brent Perteet
3ecb8e9965 ci: default releases to Jenkins build number 2026-08-24 20:24:36 -05:00
Brent Perteet
0e95da26b4 fix(ci): use portable Apple signing selector 2026-08-24 19:53:02 -05:00
Brent Perteet
bac642c221 ci: add signed iOS release pipeline 2026-08-24 19:49:44 -05:00
Brent Perteet
f6cc1b8fa4 ci: archive unsigned iOS app bundle 2026-08-24 19:23:23 -05:00
Brent Perteet
1aa3f30fce test(sync): keep contract fixture with app tests 2026-08-24 19:14:59 -05:00
7 changed files with 192 additions and 1 deletions

58
Jenkinsfile.release Normal file
View File

@@ -0,0 +1,58 @@
pipeline {
agent { label 'um-trace' }
parameters {
string(
name: 'IOS_BUILD_NUMBER',
defaultValue: '',
description: 'Optional App Store build number override. Blank uses the Jenkins build number.'
)
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time: 60, unit: 'MINUTES')
}
environment {
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
DOTNET_ROOT = '/Users/brent/.dotnet'
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
DOTNET_NOLOGO = '1'
NUGET_XMLDOC_MODE = 'skip'
PATH = "/Users/brent/.dotnet:/opt/homebrew/bin:${env.PATH}"
UM_TRACE_CODESIGN_KEY = 'Apple Distribution'
UM_TRACE_CODESIGN_PROVISION = 'UM Trace App Store'
}
stages {
stage('Toolchain and Signing') {
steps {
sh './scripts/ci/verify-macos-agent.sh'
}
}
stage('Tests') {
steps {
sh './scripts/ci/test.sh'
}
}
stage('Signed iOS IPA') {
steps {
sh './scripts/ci/build-ios-signed.sh "${IOS_BUILD_NUMBER:-$BUILD_NUMBER}"'
}
}
}
post {
always {
archiveArtifacts(
artifacts: 'artifacts/ios-signed/**/*',
allowEmptyArchive: true,
fingerprint: true
)
}
}
}

View File

@@ -43,6 +43,17 @@ The script deliberately supplies Xcode through `DEVELOPER_DIR`; it does not chan
global `xcode-select` setting. It restores only the iOS target, builds with the iOS 26 SDK, selects global `xcode-select` setting. It restores only the iOS target, builds with the iOS 26 SDK, selects
the installed `UM Trace App Store` profile, and prints the generated `.ipa` path. the installed `UM Trace App Store` profile, and prints the generated `.ipa` path.
## Create a signed IPA in Jenkins
The manually run `um-trace-ios-release` pipeline uses `Jenkinsfile.release`. By default it uses the
monotonically increasing Jenkins build number as the App Store build number. Set
`IOS_BUILD_NUMBER` only when an explicit higher override is required. The pipeline runs the tests,
signs with the distribution identity and `UM Trace App Store` profile, verifies the resulting
bundle signature and identity, and archives the IPA plus its SHA-256 file.
This pipeline only creates a signed artifact. It does not upload or submit anything to App Store
Connect, so TestFlight release remains a separate, deliberate step.
Before uploading, verify that this command lists a valid distribution identity: Before uploading, verify that this command lists a valid distribution identity:
```sh ```sh

75
scripts/ci/build-ios-signed.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
build_number="${1:-}"
codesign_key="${UM_TRACE_CODESIGN_KEY:-Apple Distribution}"
codesign_profile="${UM_TRACE_CODESIGN_PROVISION:-UM Trace App Store}"
profile_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
artifacts_dir="${repo_dir}/artifacts/ios-signed"
artifact_name="UMTrace-ios-build-${build_number}.ipa"
if [[ ! "${build_number}" =~ ^[1-9][0-9]*$ ]]; then
echo "A positive numeric iOS build number is required." >&2
exit 2
fi
if ! security find-identity -v -p codesigning | grep -Fq "${codesign_key}"; then
echo "Signing identity is unavailable: ${codesign_key}" >&2
exit 3
fi
profile_found=false
if [[ -d "${profile_dir}" ]]; then
for profile in "${profile_dir}"/*.mobileprovision; do
[[ -f "${profile}" ]] || continue
installed_name="$(openssl smime -inform der -verify -noverify -in "${profile}" 2>/dev/null | plutil -extract Name raw -o - - 2>/dev/null || true)"
if [[ "${installed_name}" == "${codesign_profile}" ]]; then
profile_found=true
break
fi
done
fi
if [[ "${profile_found}" != true ]]; then
echo "Provisioning profile is unavailable: ${codesign_profile}" >&2
exit 3
fi
UM_TRACE_CODESIGN_KEY="${codesign_key}" \
UM_TRACE_CODESIGN_PROVISION="${codesign_profile}" \
"${repo_dir}/scripts/publish-testflight.sh" "${build_number}"
source_ipa="$(find "${repo_dir}/FieldLogger/bin/Release/net9.0-ios/ios-arm64" -type f -name '*.ipa' -print -quit)"
if [[ -z "${source_ipa}" || ! -f "${source_ipa}" ]]; then
echo "The signed IPA was not created." >&2
exit 4
fi
mkdir -p "${artifacts_dir}"
ditto "${source_ipa}" "${artifacts_dir}/${artifact_name}"
verification_dir="$(mktemp -d)"
ditto -x -k "${artifacts_dir}/${artifact_name}" "${verification_dir}"
app_bundle="$(find "${verification_dir}/Payload" -maxdepth 1 -type d -name '*.app' -print -quit)"
if [[ -z "${app_bundle}" ]]; then
echo "The IPA does not contain an app bundle." >&2
exit 4
fi
codesign --verify --deep --strict --verbose=2 "${app_bundle}"
bundle_id="$(plutil -extract CFBundleIdentifier raw -o - "${app_bundle}/Info.plist")"
signed_build_number="$(plutil -extract CFBundleVersion raw -o - "${app_bundle}/Info.plist")"
if [[ "${bundle_id}" != "com.umagul.trace" || "${signed_build_number}" != "${build_number}" ]]; then
echo "Signed bundle metadata is incorrect: ${bundle_id} (${signed_build_number})." >&2
exit 5
fi
(
cd "${artifacts_dir}"
shasum -a 256 "${artifact_name}" > "${artifact_name}.sha256"
)
echo "Verified signed iOS IPA: ${artifacts_dir}/${artifact_name}"

View File

@@ -7,6 +7,9 @@ developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
project="${repo_dir}/FieldLogger/FieldLogger.csproj" project="${repo_dir}/FieldLogger/FieldLogger.csproj"
runtime_identifier="${IOS_RUNTIME:-ios-arm64}" runtime_identifier="${IOS_RUNTIME:-ios-arm64}"
nuget_cache="${UM_TRACE_NUGET_CACHE:-${TMPDIR:-/tmp}/um-trace-ci-nuget}" nuget_cache="${UM_TRACE_NUGET_CACHE:-${TMPDIR:-/tmp}/um-trace-ci-nuget}"
app_bundle="${repo_dir}/FieldLogger/bin/Debug/net9.0-ios/${runtime_identifier}/FieldLogger.app"
artifacts_dir="${repo_dir}/artifacts/ios"
artifact_name="UMTrace-unsigned-${runtime_identifier}.app.zip"
export DEVELOPER_DIR="${developer_dir}" export DEVELOPER_DIR="${developer_dir}"
export NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}" export NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}"
@@ -38,3 +41,20 @@ mkdir -p "${NUGET_HTTP_CACHE_PATH}"
-p:CodesignKey= \ -p:CodesignKey= \
-p:CodesignProvision= \ -p:CodesignProvision= \
--no-restore --no-restore
if [[ ! -d "${app_bundle}" ]]; then
echo "Unsigned app bundle was not created at ${app_bundle}." >&2
exit 3
fi
mkdir -p "${artifacts_dir}"
ditto -c -k --sequesterRsrc --keepParent \
"${app_bundle}" \
"${artifacts_dir}/${artifact_name}"
(
cd "${artifacts_dir}"
shasum -a 256 "${artifact_name}" > "${artifact_name}.sha256"
)
echo "Archived unsigned iOS app: ${artifacts_dir}/${artifact_name}"

View File

@@ -33,6 +33,12 @@ export DEVELOPER_DIR="${developer_dir}"
# referenced headless library's assets file. Restore that library explicitly before publish. # referenced headless library's assets file. Restore that library explicitly before publish.
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj" "${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj"
"${dotnet_bin}" clean "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-f net9.0-ios \
-c Release \
-p:TargetFrameworks=net9.0-ios \
-p:RuntimeIdentifier=ios-arm64
"${dotnet_bin}" publish "${repo_dir}/FieldLogger/FieldLogger.csproj" \ "${dotnet_bin}" publish "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-p:TargetFrameworks=net9.0-ios \ -p:TargetFrameworks=net9.0-ios \
-f net9.0-ios \ -f net9.0-ios \

View File

@@ -0,0 +1,21 @@
{
"schemaVersion": "1",
"jobId": "job_1",
"points": [
{
"pointId": "018f1a00-0000-7000-8000-000000000001",
"createdAt": "2026-08-21T10:00:00+00:00",
"origin": "APP",
"uploadPath": "APP_MQTT",
"lat": 40.1,
"lng": -80.2,
"ts": "2026-08-21T09:59:59+00:00",
"fix": "FIXED",
"hAcc": 0.02,
"vAcc": 0.04,
"sats": 18,
"utility": "WATER",
"qualityFlag": "IN_SPEC"
}
]
}

View File

@@ -27,7 +27,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\..\..\meta\contracts\fixtures\app-log-*.json" <None Include="ContractFixtures\app-log-*.json"
Link="ContractFixtures\%(Filename)%(Extension)" Link="ContractFixtures\%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest" /> CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>