Compare commits
7 Commits
36f583169c
...
app-owner/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ecb8e9965 | ||
|
|
0e95da26b4 | ||
|
|
bac642c221 | ||
|
|
f6cc1b8fa4 | ||
|
|
1aa3f30fce | ||
|
|
e22ae29b3f | ||
|
|
db2439c52e |
48
Jenkinsfile
vendored
Normal file
48
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label 'um-trace' }
|
||||||
|
|
||||||
|
options {
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
timeout(time: 45, 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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Toolchain') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/verify-macos-agent.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Tests') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/test.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('iOS Build (Unsigned)') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/build-ios-unsigned.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts(
|
||||||
|
artifacts: 'artifacts/**/*',
|
||||||
|
allowEmptyArchive: true,
|
||||||
|
fingerprint: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Jenkinsfile.release
Normal file
58
Jenkinsfile.release
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
75
scripts/ci/build-ios-signed.sh
Executable 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}"
|
||||||
60
scripts/ci/build-ios-unsigned.sh
Executable file
60
scripts/ci/build-ios-unsigned.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_BIN:-/Users/brent/.dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
project="${repo_dir}/FieldLogger/FieldLogger.csproj"
|
||||||
|
runtime_identifier="${IOS_RUNTIME:-ios-arm64}"
|
||||||
|
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 NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}"
|
||||||
|
|
||||||
|
mkdir -p "${NUGET_HTTP_CACHE_PATH}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" restore "${project}" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier="${runtime_identifier}" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
|
||||||
|
# A narrowed restore of the MAUI head can rewrite the referenced library's
|
||||||
|
# assets file, so restore the headless sync library explicitly as well.
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
|
||||||
|
"${dotnet_bin}" clean "${project}" \
|
||||||
|
--framework net9.0-ios \
|
||||||
|
--configuration Debug \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier="${runtime_identifier}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" build "${project}" \
|
||||||
|
--framework net9.0-ios \
|
||||||
|
--configuration Debug \
|
||||||
|
--runtime "${runtime_identifier}" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:EnableCodeSigning=false \
|
||||||
|
-p:CodesignKey= \
|
||||||
|
-p:CodesignProvision= \
|
||||||
|
--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}"
|
||||||
28
scripts/ci/test.sh
Executable file
28
scripts/ci/test.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_TEST_BIN:-/usr/local/share/dotnet/dotnet}"
|
||||||
|
results_dir="${repo_dir}/artifacts/test-results"
|
||||||
|
nuget_cache="${UM_TRACE_NUGET_CACHE:-${TMPDIR:-/tmp}/um-trace-ci-nuget}"
|
||||||
|
|
||||||
|
mkdir -p "${results_dir}" "${nuget_cache}/http-cache"
|
||||||
|
|
||||||
|
export NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}"
|
||||||
|
|
||||||
|
test_projects=(
|
||||||
|
"tests/FieldLogger.Sync.Tests/FieldLogger.Sync.Tests.csproj"
|
||||||
|
"tests/FieldLogger.Tests/FieldLogger.Tests.csproj"
|
||||||
|
"tests/IfLoc.Sim.Tests/IfLoc.Sim.Tests.csproj"
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in "${test_projects[@]}"; do
|
||||||
|
project_name="$(basename "${project}" .csproj)"
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${dotnet_bin}" restore "${repo_dir}/${project}" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${dotnet_bin}" test "${repo_dir}/${project}" \
|
||||||
|
--configuration Release \
|
||||||
|
--no-restore \
|
||||||
|
--logger "trx;LogFileName=${project_name}.trx" \
|
||||||
|
--results-directory "${results_dir}"
|
||||||
|
done
|
||||||
33
scripts/ci/verify-macos-agent.sh
Executable file
33
scripts/ci/verify-macos-agent.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dotnet_bin="${DOTNET_BIN:-/Users/brent/.dotnet/dotnet}"
|
||||||
|
test_dotnet_bin="${DOTNET_TEST_BIN:-/usr/local/share/dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
|
||||||
|
if [[ ! -x "${dotnet_bin}" ]]; then
|
||||||
|
echo "Missing Xcode 26 build SDK at ${dotnet_bin}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${test_dotnet_bin}" ]]; then
|
||||||
|
echo "Missing .NET 9 test SDK at ${test_dotnet_bin}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${developer_dir}" ]]; then
|
||||||
|
echo "Missing Xcode developer directory at ${developer_dir}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="${developer_dir}"
|
||||||
|
|
||||||
|
java -version
|
||||||
|
xcodebuild -version
|
||||||
|
"${dotnet_bin}" --info
|
||||||
|
"${dotnet_bin}" workload list
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${test_dotnet_bin}" --info
|
||||||
|
|
||||||
|
# Signing is not required for the unsigned compile build, but report what will be
|
||||||
|
# available to later approval-gated device and TestFlight stages.
|
||||||
|
security find-identity -v -p codesigning || true
|
||||||
@@ -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 \
|
||||||
|
|||||||
@@ -98,10 +98,12 @@ public sealed class SqliteOutboundStore : IOutboundStore
|
|||||||
{
|
{
|
||||||
await _db.CreateTableAsync<OutboundMessage>();
|
await _db.CreateTableAsync<OutboundMessage>();
|
||||||
// A process crash can leave rows in-flight after the broker accepted them but before
|
// A process crash can leave rows in-flight after the broker accepted them but before
|
||||||
// the application ack was applied. Requeue them; pointId makes the replay idempotent.
|
// the application ack was applied. Requeue them for immediate replay; pointId makes
|
||||||
|
// the replay idempotent. A zero due-time also keeps recovery independent of whichever
|
||||||
|
// clock implementation the sync engine uses.
|
||||||
await _db.ExecuteAsync(
|
await _db.ExecuteAsync(
|
||||||
"UPDATE outbound SET Status = ?, NextAttemptUnixMs = ? WHERE Status = ?",
|
"UPDATE outbound SET Status = ?, NextAttemptUnixMs = ? WHERE Status = ?",
|
||||||
(int)OutboundStatus.Pending, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
(int)OutboundStatus.Pending, 0,
|
||||||
(int)OutboundStatus.InFlight);
|
(int)OutboundStatus.InFlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user