Skip to content

Commit 6494b27

Browse files
committed
ci: make dependency resolution resilient with or without a cache
Jobs were being killed at the job timeout with no build output at all, and cleanup terminating orphaned git and git-remote-http processes. They were not hung on a test or a build — they were still fetching dependencies. Left to xcodebuild, resolution happens silently inside the build or test invocation. SwiftPM checks out ~31 repositories one at a time through blocking git subprocesses and prints nothing until the first compile, so a slow clone is indistinguishable from a hang: the job emits no output, and is eventually killed with the whole run lost. Resolution is now an explicit step in both run_xcodebuild and run_xcodebuild_test: - bounded per attempt by a background watchdog, so a stalled clone is killed rather than left to consume the job budget - retried up to 3 times, clearing partial mirror state between attempts, since a half-written clone is a common way for this to wedge - logged in its own group with elapsed time, so slow is distinguishable from stuck - failing with an explicit "this is a dependency fetch problem, not a test failure" rather than a bare timeout The build and test invocations now always pass -disableAutomaticPackageResolution. Whether the checkouts came from a warm cache or from the step above, xcodebuild can never fall back to resolving mid-run, which is the fallback that produced the silent hangs. The watchdog is used because `timeout-minutes:` is rejected on steps inside a composite action and `timeout(1)` is not installed on macOS runners. Job timeouts go from 30 to 60 minutes, including the eight callers that hardcoded 30 and so overrode the reusable workflow default. That is headroom for cold fetches and for simulator startup, which has been observed taking tens of minutes on visionOS and iOS; it is not the fix, since resolution is separately bounded above. Deliberately not included: cache seeding. A seeding workflow only runs once it is on the default branch, and GitHub scopes branch caches so a cache seeded on one branch is invisible to sibling PRs. That made it unverifiable before merge and useless to the PRs that needed it. This change instead makes the cold path work on its own; caching remains a speed optimisation rather than a correctness requirement. Verified locally against a completely empty cache (all amplify-packages entries deleted first): resolve took 2m11s and produced 31 checkouts, then `xcodebuild test` with resolution disabled took 1m43s and passed (** TEST SUCCEEDED **, 31 tests), with zero fetch attempts during the test. Also confirmed `swift package resolve` is not a faster substitute — it took 4m19s against xcodebuild's 2m11s.
1 parent ff17f56 commit 6494b27

14 files changed

Lines changed: 286 additions & 20 deletions

.github/composite_actions/run_xcodebuild/action.yml

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,59 @@ inputs:
3939
runs:
4040
using: "composite"
4141
steps:
42+
# See run_xcodebuild_test/action.yml for the full rationale. In short:
43+
# resolution runs as its own bounded, retried, logged step so a slow git clone
44+
# cannot masquerade as a hang and burn the job's entire timeout with no output.
45+
- name: Resolve package dependencies for ${{ inputs.scheme }}
46+
shell: bash
47+
env:
48+
PROJECT_PATH: ${{ inputs.project_path }}
49+
XCODE_PATH: ${{ inputs.xcode_path }}
50+
CLONED_SOURCE_PACKAGES_PATH: ${{ inputs.cloned_source_packages_path }}
51+
RESOLVE_TIMEOUT_SECONDS: '1500'
52+
run: |
53+
set -o pipefail
54+
if [ -n "$PROJECT_PATH" ]; then cd "$PROJECT_PATH"; fi
55+
if [ -n "$XCODE_PATH" ]; then sudo xcode-select -s "$XCODE_PATH"; fi
56+
57+
clonedPath=()
58+
if [ -n "$CLONED_SOURCE_PACKAGES_PATH" ]; then
59+
clonedPath=(-clonedSourcePackagesDirPath "$CLONED_SOURCE_PACKAGES_PATH")
60+
fi
61+
62+
resolve_bounded() {
63+
xcodebuild -resolvePackageDependencies \
64+
-scheme '${{ inputs.scheme }}' \
65+
"${clonedPath[@]}" &
66+
local pid=$!
67+
( sleep "$RESOLVE_TIMEOUT_SECONDS"; kill -9 "$pid" 2>/dev/null ) &
68+
local watchdog=$!
69+
wait "$pid"
70+
local status=$?
71+
kill "$watchdog" 2>/dev/null || true
72+
wait "$watchdog" 2>/dev/null || true
73+
return $status
74+
}
75+
76+
for attempt in 1 2 3; do
77+
echo "::group::Resolving package dependencies (attempt $attempt of 3)"
78+
start=$(date +%s)
79+
if resolve_bounded; then
80+
echo "::endgroup::"
81+
echo "Dependencies resolved in $(( $(date +%s) - start ))s on attempt $attempt."
82+
exit 0
83+
fi
84+
echo "::endgroup::"
85+
echo "Attempt $attempt failed or exceeded ${RESOLVE_TIMEOUT_SECONDS}s after $(( $(date +%s) - start ))s."
86+
if [ -n "$CLONED_SOURCE_PACKAGES_PATH" ]; then
87+
rm -rf "${CLONED_SOURCE_PACKAGES_PATH%/}/repositories" || true
88+
fi
89+
done
90+
91+
echo "::error::Could not resolve package dependencies after 3 attempts. \
92+
This is a dependency fetch problem, not a build failure."
93+
exit 1
94+
4295
- name: Test ${{ inputs.scheme }}
4396
env:
4497
SCHEME: ${{ inputs.scheme }}
@@ -55,10 +108,10 @@ runs:
55108
fi
56109
57110
otherFlags="${{ inputs.other_flags }}"
58-
if [ "${{ inputs.disable_package_resolution }}" == "true" ]; then
59-
echo "Disabling Automatic Package Resolution"
60-
otherFlags+=" -disableAutomaticPackageResolution"
61-
fi
111+
# Unconditionally disabled — the resolve step above already produced the
112+
# checkouts, so xcodebuild must never resolve mid-build.
113+
echo "Disabling Automatic Package Resolution"
114+
otherFlags+=" -disableAutomaticPackageResolution"
62115
63116
if [ ! -z "$DERIVED_DATA_PATH" ]; then
64117
echo "Using custom DerivedData path"

.github/composite_actions/run_xcodebuild_test/action.yml

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,79 @@ inputs:
4646
runs:
4747
using: "composite"
4848
steps:
49+
# Resolve dependencies as an explicit, separately bounded step.
50+
#
51+
# Left to xcodebuild, resolution happens silently inside the test invocation.
52+
# SwiftPM checks out ~31 repositories one at a time through blocking git
53+
# subprocesses and prints nothing until the first compile, so a slow clone is
54+
# indistinguishable from a hang: the job produces no output and is eventually
55+
# killed at the job timeout, taking the test run with it. Observed failures had
56+
# zero build output and orphaned git / git-remote-http processes.
57+
#
58+
# Doing it here means resolution has its own clock and its own log group, is
59+
# retried for transient git/TLS failures, and — if it genuinely cannot finish —
60+
# fails fast with a clear reason instead of consuming the whole job budget.
61+
#
62+
# The bound uses a background watchdog rather than `timeout-minutes:`, which
63+
# GitHub rejects on steps inside a composite action, and rather than
64+
# `timeout(1)`, which is not installed on macOS runners.
65+
- name: Resolve package dependencies for ${{ inputs.scheme }}
66+
shell: bash
67+
env:
68+
PROJECT_PATH: ${{ inputs.project_path }}
69+
XCODE_PATH: ${{ inputs.xcode_path }}
70+
CLONED_SOURCE_PACKAGES_PATH: ${{ inputs.cloned_source_packages_path }}
71+
# A cold resolve is ~2 minutes locally but far slower on a loaded runner,
72+
# where many jobs clone the same graph at once. 25 minutes per attempt is
73+
# sized to absorb that while still being well short of the job timeout, so
74+
# a genuine stall surfaces as a fast, explicit failure.
75+
RESOLVE_TIMEOUT_SECONDS: '1500'
76+
run: |
77+
set -o pipefail
78+
if [ -n "$PROJECT_PATH" ]; then cd "$PROJECT_PATH"; fi
79+
if [ -n "$XCODE_PATH" ]; then sudo xcode-select -s "$XCODE_PATH"; fi
80+
81+
clonedPath=()
82+
if [ -n "$CLONED_SOURCE_PACKAGES_PATH" ]; then
83+
clonedPath=(-clonedSourcePackagesDirPath "$CLONED_SOURCE_PACKAGES_PATH")
84+
fi
85+
86+
resolve_bounded() {
87+
xcodebuild -resolvePackageDependencies \
88+
-scheme '${{ inputs.scheme }}' \
89+
"${clonedPath[@]}" &
90+
local pid=$!
91+
( sleep "$RESOLVE_TIMEOUT_SECONDS"; kill -9 "$pid" 2>/dev/null ) &
92+
local watchdog=$!
93+
wait "$pid"
94+
local status=$?
95+
kill "$watchdog" 2>/dev/null || true
96+
wait "$watchdog" 2>/dev/null || true
97+
return $status
98+
}
99+
100+
for attempt in 1 2 3; do
101+
echo "::group::Resolving package dependencies (attempt $attempt of 3)"
102+
start=$(date +%s)
103+
if resolve_bounded; then
104+
echo "::endgroup::"
105+
echo "Dependencies resolved in $(( $(date +%s) - start ))s on attempt $attempt."
106+
exit 0
107+
fi
108+
echo "::endgroup::"
109+
echo "Attempt $attempt failed or exceeded ${RESOLVE_TIMEOUT_SECONDS}s after $(( $(date +%s) - start ))s."
110+
111+
# Clear partial mirror state so the next attempt is not resuming a
112+
# half-written clone, which is a common way for this to wedge.
113+
if [ -n "$CLONED_SOURCE_PACKAGES_PATH" ]; then
114+
rm -rf "${CLONED_SOURCE_PACKAGES_PATH%/}/repositories" || true
115+
fi
116+
done
117+
118+
echo "::error::Could not resolve package dependencies after 3 attempts. \
119+
This is a dependency fetch problem, not a test failure."
120+
exit 1
121+
49122
- name: Test ${{ inputs.scheme }}
50123
env:
51124
SCHEME: ${{ inputs.scheme }}
@@ -87,10 +160,11 @@ runs:
87160
fi
88161
fi
89162
90-
if [ "${{ inputs.disable_package_resolution }}" == "true" ]; then
91-
echo "Disabling Automatic Package Resolution"
92-
clonedSourcePackagesPath+=" -disableAutomaticPackageResolution"
93-
fi
163+
# Unconditionally disabled. The resolve step above already produced the
164+
# checkouts (or failed loudly trying), so xcodebuild must never fall back
165+
# to resolving here — that fallback is the silent hang this avoids.
166+
echo "Disabling Automatic Package Resolution"
167+
clonedSourcePackagesPath+=" -disableAutomaticPackageResolution"
94168
95169
action="test"
96170
if [ "${{ inputs.test_without_building }}" == "true" ]; then
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: Build | Xcode Preview (Beta)
2+
3+
# Early warning for the next Xcode release.
4+
#
5+
# Runs against the `xcode-27` preview runner image, which ships a single beta
6+
# Xcode as the image default. It deliberately does not use the
7+
# get_platform_parameters composite action: that action allowlists only the Xcode
8+
# versions Amplify Swift officially supports, and a beta is outside that set by
9+
# design.
10+
#
11+
# Advisory and non-blocking. Every job sets continue-on-error, so breakages from
12+
# the next Xcode surface on the PR that introduces them without ever gating a
13+
# merge. A beta toolchain on a capacity-constrained preview image is not a
14+
# suitable required check.
15+
16+
on:
17+
pull_request:
18+
branches:
19+
- main
20+
push:
21+
branches:
22+
- main
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: read
27+
28+
concurrency:
29+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
30+
cancel-in-progress: ${{ github.ref_name != 'main' }}
31+
32+
jobs:
33+
build:
34+
name: ${{ matrix.platform }} on Xcode preview
35+
runs-on: xcode-27
36+
continue-on-error: true
37+
timeout-minutes: 60
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
include:
42+
- platform: macOS
43+
destination: 'platform=macOS,arch=arm64'
44+
- platform: iOS
45+
# Generic rather than a named device: the preview image's simulator
46+
# lineup changes between beta rollouts, so a pinned device name is a
47+
# guaranteed future break. Generic cannot boot a simulator, so this
48+
# leg builds rather than tests.
49+
destination: 'generic/platform=iOS Simulator'
50+
51+
steps:
52+
- name: Checkout repository
53+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
54+
with:
55+
persist-credentials: false
56+
57+
- name: Report the toolchain under test
58+
run: |
59+
# Use the image default Xcode rather than a hardcoded path — the beta
60+
# version string changes between image rollouts.
61+
xcodebuild -version
62+
swift --version
63+
64+
# Same reasoning as the run_xcodebuild* composite actions: resolve as its
65+
# own bounded, retried step so a slow fetch cannot look like a hang.
66+
- name: Resolve package dependencies
67+
env:
68+
RESOLVE_TIMEOUT_SECONDS: '1500'
69+
run: |
70+
set -o pipefail
71+
resolve_bounded() {
72+
xcodebuild -resolvePackageDependencies -scheme Amplify-Package &
73+
local pid=$!
74+
( sleep "$RESOLVE_TIMEOUT_SECONDS"; kill -9 "$pid" 2>/dev/null ) &
75+
local watchdog=$!
76+
wait "$pid"
77+
local status=$?
78+
kill "$watchdog" 2>/dev/null || true
79+
wait "$watchdog" 2>/dev/null || true
80+
return $status
81+
}
82+
83+
for attempt in 1 2; do
84+
echo "::group::Resolving package dependencies (attempt $attempt of 2)"
85+
if resolve_bounded; then
86+
echo "::endgroup::"
87+
exit 0
88+
fi
89+
echo "::endgroup::"
90+
echo "Attempt $attempt failed or timed out."
91+
done
92+
93+
echo "::error::Could not resolve package dependencies."
94+
exit 1
95+
96+
- name: Build Amplify Swift for ${{ matrix.platform }}
97+
run: |
98+
set -o pipefail
99+
# xcbeautify is preinstalled on the standard macOS images but the preview
100+
# image is built separately, so fall back to raw output rather than
101+
# failing on a missing formatter.
102+
if command -v xcbeautify >/dev/null 2>&1; then
103+
formatter=(xcbeautify --renderer github-actions)
104+
else
105+
echo "xcbeautify not present on this image; using raw xcodebuild output."
106+
formatter=(cat)
107+
fi
108+
109+
xcodebuild build \
110+
-scheme Amplify-Package \
111+
-destination '${{ matrix.destination }}' \
112+
-skipPackagePluginValidation \
113+
-disableAutomaticPackageResolution \
114+
| "${formatter[@]}" && exit ${PIPESTATUS[0]}
115+
116+
report:
117+
name: Report Preview Status
118+
runs-on: ubuntu-latest
119+
if: ${{ !cancelled() }}
120+
needs: [ build ]
121+
steps:
122+
- name: Summarize
123+
run: |
124+
RESULT="${{ needs.build.result }}"
125+
if [ "$RESULT" = "success" ]; then
126+
echo "Amplify Swift builds cleanly on the Xcode preview toolchain." >> $GITHUB_STEP_SUMMARY
127+
else
128+
echo "Amplify Swift hit failures on the Xcode preview toolchain (result: $RESULT)." >> $GITHUB_STEP_SUMMARY
129+
echo "This is advisory only and does not block this PR. Investigate before the next Xcode reaches GA." >> $GITHUB_STEP_SUMMARY
130+
fi

.github/workflows/integ_test_analytics.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ jobs:
4040
platform: ${{ matrix.platform }}
4141
project_path: ./AmplifyPlugins/Analytics/Tests/AnalyticsHostApp
4242
resource_subfolder: analytics
43-
timeout-minutes: 30
43+
timeout-minutes: 60
4444
secrets: inherit

.github/workflows/integ_test_auth.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
platform: ${{ matrix.platform }}
5151
project_path: ./AmplifyPlugins/Auth/Tests/AuthHostApp/
5252
resource_subfolder: auth
53-
timeout-minutes: 30
53+
timeout-minutes: 60
5454
secrets: inherit
5555

5656
auth-ui-integration-test-iOS:
@@ -61,7 +61,7 @@ jobs:
6161
platform: iOS
6262
project_path: ./AmplifyPlugins/Auth/Tests/AuthHostedUIApp/
6363
resource_subfolder: auth
64-
timeout-minutes: 30
64+
timeout-minutes: 60
6565
xcode_version: '16.4.0'
6666
secrets: inherit
6767

.github/workflows/integ_test_auth_webauthn.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
auth-webauthn-integration-tests:
1616
name: iOS Tests | AuthWebAuthnApp
1717
runs-on: macos-15
18-
timeout-minutes: 30
18+
timeout-minutes: 60
1919
environment: IntegrationTest
2020

2121
steps:

.github/workflows/integ_test_geo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ jobs:
4040
platform: ${{ matrix.platform }}
4141
project_path: ./AmplifyPlugins/Geo/Tests/GeoHostApp/
4242
resource_subfolder: geo
43-
timeout-minutes: 30
43+
timeout-minutes: 60
4444
secrets: inherit

.github/workflows/integ_test_kinesis_firehose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ jobs:
3434
platform: ${{ matrix.platform }}
3535
project_path: ./AmplifyClients/Tests/IntegrationTests/KinesisFirehoseClientHostApp
3636
resource_subfolder: kinesis
37-
timeout-minutes: 30
37+
timeout-minutes: 60
3838
secrets: inherit

.github/workflows/integ_test_predictions.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ jobs:
4040
platform: ${{ matrix.platform }}
4141
project_path: ./AmplifyPlugins/Predictions/Tests/PredictionsHostApp
4242
resource_subfolder: predictions
43-
timeout-minutes: 30
43+
timeout-minutes: 60
4444
secrets: inherit

.github/workflows/integ_test_push_notifications.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
push-notification-integration-tests:
2828
name: ${{ matrix.platform }} Tests | PushNotificationHostApp
2929
runs-on: macos-15
30-
timeout-minutes: 30
30+
timeout-minutes: 60
3131
environment: IntegrationTest
3232
strategy:
3333
fail-fast: false

0 commit comments

Comments
 (0)