-
Notifications
You must be signed in to change notification settings - Fork 2
636 lines (588 loc) · 32.5 KB
/
Copy pathquickstart.yml
File metadata and controls
636 lines (588 loc) · 32.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
name: Quickstart
# Coverage note (#1755):
#
# Two guarantees have no equivalent in-repo test and are the reason this
# workflow still runs:
# * Horizon ingest pipeline — a real horizon process consumes
# LedgerCloseMeta from a henyey validator end-to-end (pubnet shard).
# * Multi-service orchestration — docker startup order, port binding, and
# inter-service pipes when core/rpc/horizon run together.
#
# Three other guarantees that previously only ran here now have fast
# deterministic in-repo tests:
# * Two-process catchup → run handoff:
# crates/henyey/tests/catchup_to_run_handoff.rs
# * LedgerCloseMeta record-mark framing on fd:3: covered by the same
# subprocess test reading via `XdrInputStream` (wire-compat-tested
# against stellar-core at crates/common/src/xdr_stream.rs).
# * JSON-RPC HTTP surface: crates/rpc/tests/http_dispatch.rs.
#
# Test orchestration (#2916):
#
# Image building is delegated to stellar/quickstart's reusable build
# workflow with test: false. Test orchestration lives in this repo so we
# can apply a timeout-only retry to the flaky testnet/core,horizon shard
# without forking the entire upstream test matrix.
# Each probe is run through scripts/ci/run-quickstart-test.sh which adds:
# * GNU timeout with diagnostics capture on failure
# * Exactly one retry for ANY probe on the testnet/core,horizon shard when the
# exit is a transient-infra signature: exit 124 (timeout / slow start, #2916)
# or exit 143 (SIGTERM — "runner has received a shutdown signal" /
# spot-runner reclamation, #3131). Scope was widened from the single
# horizon-core-up probe to the whole shard in #3185: testnet stellar-core's
# slow catchup propagates to whichever probe runs next (horizon-ingesting
# timed out right after horizon-core-up came up — run 27019344504), and that
# probe had no retry. The retry stays scoped to this one shard.
# * No retry for any other shard, or for any non-transient failure (e.g. a
# genuine probe failure exit 1) — those still fail loudly.
# * Soft-skip on testnet TIMEOUT (#3272): the testnet/core,horizon shard alone
# passes --soft-on-timeout, so after the retry is exhausted a probe TIMEOUT
# (exit 124 ONLY) is converted to a neutral exit 0 with a grep-able
# SOFT-SKIP marker — the testnet shard depends on external network liveness
# (slow catchup / checkpoint cadence / archive availability), not henyey
# correctness, and a stuck sync probe used to red-roll the whole workflow.
# A genuine assertion failure (any non-124 exit) on testnet STILL stays red,
# so a real henyey-on-testnet break is never masked. Paired with a tighter
# 240s per-probe timeout on that shard so the soft-skip fires in minutes.
#
# Whole-runner reclamation (#3185 / #3193): when GitHub SIGTERMs/SIGKILLs the
# entire runner, the bash wrapper is killed too and never reaches its in-script
# retry. An in-RUN job cannot recover this — a job inside a run cannot
# re-dispatch (rerun --failed) its own still-running run (it fails with
# "This workflow is already running", #3193). Recovery therefore lives in a
# SEPARATE workflow_run-triggered workflow, .github/workflows/quickstart-retry.yml,
# which fires AFTER this run completes (so the rerun is accepted) and
# re-dispatches the failed jobs exactly once (conclusion == failure AND
# run_attempt == 1). A genuine failure reproduces on attempt 2 and stays red,
# so this absorbs exactly one transient reclamation without masking regressions.
#
# Upstream contract validation:
# The validate-contract job fetches both build.yml and internal-build.yml
# from the resolved SHA and validates them against the pinned contract at
# scripts/ci/upstream-quickstart-contract.yml. This catches drift in the
# input interface (build.yml) and the artifact/image interface
# (internal-build.yml). Note: the `uses:` field calls build.yml@main
# because GitHub Actions does not support dynamic refs for reusable
# workflows. See the contract file header for the full limitation.
#
# Testnet shard (#1848): Temporarily downgraded from core,rpc,horizon to
# core,horizon. The test_stellar_rpc_healthy step requires continuous
# ledger close, but after initial catchup the node waits up to ~5 min
# for the next archive checkpoint (64-ledger cycle on testnet). The
# upstream timeout_multiplier is hardcoded to 4 (= 4 min on first
# attempt), which is insufficient. Re-enable RPC on testnet when the
# upstream timeout is configurable or the RPC healthy test is skipped
# for testnet (matching the pubnet precedent).
permissions:
contents: read
# Per-probe timeout budget, mirroring upstream
# stellar/quickstart/.github/workflows/internal-test.yml, which sets
# `env.timeout_multiplier: 4` and computes each probe step's timeout-minutes as
# `github.run_attempt * timeout_multiplier`. We reproduce the same multiplier
# here so the in-repo orchestration preserves upstream timeout semantics (4 min
# on attempt 1, escalating on manual re-runs) instead of diverging silently.
# Pinned in scripts/ci/upstream-quickstart-contract.yml (timeout_multiplier: 4).
# Note: the validate-contract job only fetches/validates upstream build.yml and
# internal-build.yml, not internal-test.yml (where the multiplier lives), so it
# does NOT catch multiplier drift. Drift is caught by the harness/text
# assertions in scripts/test-quickstart-harness.sh instead (#2920).
env:
timeout_multiplier: 4
on:
pull_request:
push:
branches: [main]
paths-ignore:
- '*.md'
- 'docs/**'
- 'prompts/**'
- 'reports/**'
- 'ai-summary/**'
- '.claude/**'
- '.github/skills/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
# Quickstart runs take 15–30 min and are expensive to restart. Let the
# current run finish; GitHub collapses intermediate queued runs so only
# the most recent queued run executes after the current one completes.
cancel-in-progress: false
jobs:
# Resolve the quickstart SHA once so build and test use the same version.
setup:
if: github.event_name == 'push' || github.event.pull_request.head.repo.private == false
runs-on: ubuntu-latest
outputs:
quickstart-sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Resolve stellar/quickstart SHA
id: resolve
run: |
SHA=$(git ls-remote https://github.com/stellar/quickstart.git HEAD | cut -f1)
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "Resolved stellar/quickstart SHA: $SHA"
# Validate that our assumptions about the upstream contract still hold.
# Fetches both the top-level build.yml (input contract) and the delegated
# internal-build.yml (artifact/image contract) from the resolved SHA, then
# validates both against scripts/ci/upstream-quickstart-contract.yml.
#
# The drift guard step verifies the resolved SHA == current main HEAD,
# since GitHub Actions requires the literal `@main` ref in `uses:`. If they
# diverge (e.g., setup resolved a stale SHA), the job fails immediately
# rather than validating one revision while executing another.
validate-contract:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify resolved SHA matches @main (drift guard)
run: |
SHA="${{ needs.setup.outputs.quickstart-sha }}"
# We call build.yml@main but validate at $SHA. If main has advanced
# past the SHA we resolved in setup, the executed workflow may differ
# from what we validated — fail fast to avoid silent false-greens.
MAIN_SHA=$(git ls-remote https://github.com/stellar/quickstart.git refs/heads/main | cut -f1)
if [[ "$SHA" != "$MAIN_SHA" ]]; then
echo "✗ Resolved SHA ($SHA) differs from current main ($MAIN_SHA)."
echo " The build job calls build.yml@main but we validated at $SHA."
echo " This means the executed workflow may have drifted from what"
echo " we validated. Re-run the workflow to pick up the latest SHA."
exit 1
fi
echo "✓ Resolved SHA matches current main — no @main drift."
- name: Fetch upstream workflows
run: |
SHA="${{ needs.setup.outputs.quickstart-sha }}"
curl -sf "https://raw.githubusercontent.com/stellar/quickstart/$SHA/.github/workflows/build.yml" \
-o /tmp/upstream-build.yml
# internal-build.yml is the delegated workflow that produces artifacts.
# It may not exist (older upstream versions inline it) — handle gracefully.
curl -sf "https://raw.githubusercontent.com/stellar/quickstart/$SHA/.github/workflows/internal-build.yml" \
-o /tmp/upstream-internal-build.yml || true
- name: Validate upstream contract
run: |
CONTRACT="scripts/ci/upstream-quickstart-contract.yml"
UPSTREAM="/tmp/upstream-build.yml"
INTERNAL="/tmp/upstream-internal-build.yml"
echo "=== Validating upstream quickstart contract ==="
echo "SHA: ${{ needs.setup.outputs.quickstart-sha }}"
echo "Contract file: $CONTRACT"
echo ""
ERRORS=0
# --- 1. Validate build.yml input contract ---
echo "--- build.yml input contract ---"
# Read expected inputs from contract file
if ! grep -q '^build_inputs:' "$CONTRACT"; then
echo "✗ contract file missing build_inputs section"
ERRORS=$((ERRORS + 1))
else
for input in ref test images archs; do
if grep -q " - $input" "$CONTRACT"; then
if grep -q "$input:" "$UPSTREAM"; then
echo "✓ upstream accepts '$input' input (matches contract)"
else
echo "✗ upstream missing '$input' input (contract expects it)"
ERRORS=$((ERRORS + 1))
fi
fi
done
fi
# Verify 'test' is boolean-typed (we depend on test: false)
if grep -q 'test:' "$UPSTREAM" && grep -A5 'test:' "$UPSTREAM" | grep -q 'type: boolean\|type:.*bool'; then
echo "✓ 'test' input is boolean-typed"
else
echo "✗ 'test' input not found or not boolean-typed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# --- 2. Validate artifact/image contract (internal-build.yml) ---
echo "--- internal-build.yml artifact contract ---"
# Read expected artifact name from contract
EXPECTED_ARTIFACT=$(grep '^expected_artifact_name:' "$CONTRACT" | sed 's/.*: *"\(.*\)"/\1/')
EXPECTED_TAG=$(grep '^expected_image_tag:' "$CONTRACT" | sed 's/.*: *"\(.*\)"/\1/')
ARTIFACT_PATTERN=$(grep '^artifact_name_pattern:' "$CONTRACT" | sed 's/.*: *"\(.*\)"/\1/')
if [[ -f "$INTERNAL" ]]; then
echo "✓ internal-build.yml found at resolved SHA"
# Check that it uploads artifacts (upload-artifact action present)
if grep -q 'upload-artifact' "$INTERNAL"; then
echo "✓ internal-build.yml uploads artifacts"
else
echo "✗ internal-build.yml does not use upload-artifact"
ERRORS=$((ERRORS + 1))
fi
# --- Exact artifact name validation ---
# The contract specifies artifact_name_pattern: "image-quickstart-{tag}-{arch}.tar"
# Upstream internal-build.yml constructs the artifact name using expressions like:
# image-quickstart-<inputs.tag expr>-<matrix.arch expr>
# (the angle-bracket placeholders stand in for GHA expression syntax;
# literal expression delimiters are avoided here so the workflow file
# itself stays parseable — see #2933.)
# We validate: (a) the exact "image-quickstart-" prefix with tag/arch interpolation,
# and (b) our concrete expected_artifact_name decomposes correctly from the pattern.
#
# Convert pattern "image-quickstart-{tag}-{arch}.tar" into a regex that matches
# the upstream template expression (tag and arch are GHA expressions).
ARTIFACT_PREFIX="image-quickstart-"
if grep -q "${ARTIFACT_PREFIX}" "$INTERNAL"; then
echo "✓ internal-build.yml artifact name uses prefix '${ARTIFACT_PREFIX}'"
else
echo "✗ internal-build.yml missing artifact name prefix '${ARTIFACT_PREFIX}'"
echo " Expected pattern: $ARTIFACT_PATTERN"
echo " Expected concrete: $EXPECTED_ARTIFACT"
ERRORS=$((ERRORS + 1))
fi
# Validate the artifact name template follows the exact {tag}-{arch}.tar structure.
# Upstream must produce: image-quickstart-<tag_expr>-<arch_expr> (with .tar optional
# in the upload name). We check that the line contains the prefix, then a tag
# expression, a literal hyphen separator, then an arch expression — in that order.
# This rejects incompatible shapes like {arch}-{tag} or {tag}.zip.
ARTIFACT_REGEX="${ARTIFACT_PREFIX}"'\$\{\{[^}]*(tag|inputs\.tag|matrix\.tag)[^}]*\}\}-\$\{\{[^}]*(arch|matrix\.arch|inputs\.arch)[^}]*\}\}'
if grep -qE "$ARTIFACT_REGEX" "$INTERNAL"; then
echo "✓ internal-build.yml artifact name matches {tag}-{arch} structure"
else
echo "✗ internal-build.yml artifact name does not match expected {tag}-{arch} structure"
echo " Expected regex: $ARTIFACT_REGEX"
echo " Expected pattern: $ARTIFACT_PATTERN (tag=testing-with-pr, arch=amd64)"
ERRORS=$((ERRORS + 1))
fi
# --- Exact image tag validation ---
# The contract specifies image_tag_pattern: "quickstart:{tag}-{arch}"
# Upstream tags the image as quickstart:<inputs.tag expr>-<matrix.arch expr>
# or similar. Validate the "quickstart:" prefix with tag-arch interpolation.
IMAGE_TAG_PREFIX="quickstart:"
if grep -q "${IMAGE_TAG_PREFIX}" "$INTERNAL"; then
echo "✓ internal-build.yml image tag uses prefix '${IMAGE_TAG_PREFIX}'"
else
echo "✗ internal-build.yml missing image tag prefix '${IMAGE_TAG_PREFIX}'"
echo " Expected tag pattern: quickstart:{tag}-{arch}"
echo " Expected concrete: $EXPECTED_TAG"
ERRORS=$((ERRORS + 1))
fi
# Validate the image tag template follows the exact {tag}-{arch} structure.
# Upstream must tag as: quickstart:<tag_expr>-<arch_expr>.
# Reject shapes like quickstart:<inputs.tag expr> (missing arch).
TAG_REGEX="${IMAGE_TAG_PREFIX}"'\$\{\{[^}]*(tag|inputs\.tag|matrix\.tag)[^}]*\}\}-\$\{\{[^}]*(arch|matrix\.arch|inputs\.arch)[^}]*\}\}'
if grep -qE "$TAG_REGEX" "$INTERNAL"; then
echo "✓ internal-build.yml image tag matches {tag}-{arch} structure"
else
echo "✗ internal-build.yml image tag does not match expected {tag}-{arch} structure"
echo " Expected regex: $TAG_REGEX"
echo " Expected: dynamic tag with {tag}-{arch} pattern"
ERRORS=$((ERRORS + 1))
fi
# Check docker save / image export pattern (/tmp/image is the conventional path)
if grep -q '/tmp/image\|docker save' "$INTERNAL"; then
echo "✓ internal-build.yml uses docker image export (matches artifact_layout)"
else
echo "✗ could not confirm /tmp/image or docker save in internal-build.yml"
echo " The 'Load Docker image' step depends on this layout — drift will break test runs."
ERRORS=$((ERRORS + 1))
fi
else
# internal-build.yml not found — check if build.yml itself handles artifacts
echo "⚠ internal-build.yml not found at resolved SHA (may be inlined in build.yml)"
if grep -q "${ARTIFACT_PREFIX:-image-quickstart-}" "$UPSTREAM" && grep -q 'upload-artifact' "$UPSTREAM"; then
echo "✓ build.yml directly handles artifact upload with expected naming"
else
echo "✗ neither build.yml nor internal-build.yml confirms artifact contract"
echo " Expected artifact: $EXPECTED_ARTIFACT"
ERRORS=$((ERRORS + 1))
fi
fi
echo ""
# --- 2b. Validate OUR consumer-side artifact/tag shape (anchored) ---
# The checks above validate the UPSTREAM template (internal-build.yml).
# This block validates the EXACT, concrete artifact filename and image
# tag that *this* workflow consumes — the download-artifact `name:` and
# the `docker run ... <tag>` line — against anchored ERE regexes stored
# in the contract file (single source of truth, also used by
# scripts/test-quickstart-harness.sh). Anchoring rejects incompatible
# suffixes (.zip, .tar.gz, -debug) that a bare substring `grep -q`
# would silently accept (#2932). The contract stores the regex body
# verbatim (raw grep|sed extraction, no YAML unescaping).
echo "--- consumer-side artifact/tag shape (this workflow) ---"
ARTIFACT_NAME_REGEX=$(grep '^artifact_name_regex:' "$CONTRACT" | sed 's/^artifact_name_regex: *"\(.*\)"$/\1/')
IMAGE_TAG_REGEX=$(grep '^image_tag_regex:' "$CONTRACT" | sed 's/^image_tag_regex: *"\(.*\)"$/\1/')
WORKFLOW_SELF=".github/workflows/quickstart.yml"
if [[ -z "$ARTIFACT_NAME_REGEX" || -z "$IMAGE_TAG_REGEX" ]]; then
echo "✗ contract missing artifact_name_regex/image_tag_regex (anchored ERE)"
ERRORS=$((ERRORS + 1))
else
if grep -Eq "$ARTIFACT_NAME_REGEX" "$WORKFLOW_SELF"; then
echo "✓ workflow downloads the exact artifact shape (anchored)"
else
echo "✗ workflow has no line matching the anchored artifact regex"
echo " Regex: $ARTIFACT_NAME_REGEX"
ERRORS=$((ERRORS + 1))
fi
if grep -Eq "$IMAGE_TAG_REGEX" "$WORKFLOW_SELF"; then
echo "✓ workflow runs the exact image tag shape (anchored)"
else
echo "✗ workflow has no line matching the anchored image-tag regex"
echo " Regex: $IMAGE_TAG_REGEX"
ERRORS=$((ERRORS + 1))
fi
fi
echo ""
# --- 3. Summary ---
if [[ $ERRORS -gt 0 ]]; then
echo "=== FAILED: $ERRORS contract violation(s) detected ==="
echo "The upstream stellar/quickstart workflows at SHA ${{ needs.setup.outputs.quickstart-sha }}"
echo "no longer match our pinned contract at $CONTRACT."
echo "Update the contract file and workflow if the upstream change is intentional."
exit 1
fi
echo "=== Upstream contract validation passed (${{ needs.setup.outputs.quickstart-sha }}) ==="
# Build the quickstart image with test: false — testing is handled locally.
#
# NOTE: GitHub Actions does not support dynamic refs in `uses:` for reusable
# workflows. We call build.yml@main; the `validate-contract` job's drift
# guard ensures the resolved SHA == main HEAD before we reach this point,
# so the validated and executed definitions are the same revision.
build:
needs: [setup, validate-contract]
uses: stellar/quickstart/.github/workflows/build.yml@main
with:
ref: ${{ needs.setup.outputs.quickstart-sha }}
test: false
images: |
[
{
"tag": "testing-with-pr",
"inherit": "testing",
"config": {
"horizon_skip_protocol_version_check": true
},
"deps": [
{ "name": "core", "repo": "${{ github.event.pull_request.head.repo.full_name || github.repository }}", "ref": "${{ github.event.pull_request.head.sha || github.sha }}" }
]
}
]
archs: '["amd64"]'
# Run upstream Go probes locally through the timeout/retry wrapper.
test:
needs: [setup, build]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
# Local shards (network=local)
- network: local
enable: core
probes: "test_core.go"
- network: local
enable: rpc
probes: "test_stellar_rpc_up.go test_stellar_rpc_healthy.go test_friendbot.go"
- network: local
enable: "core,rpc,horizon"
probes: "test_core.go test_horizon_up.go test_horizon_core_up.go test_horizon_ingesting.go test_stellar_rpc_up.go test_stellar_rpc_healthy.go test_friendbot.go"
# local/galexie soft-degate (#3563). The pinned galexie image
# (galexie-v26.1.0) never exports a ledger-meta partition while
# henyey core now runs Protocol 27, so test_galexie.go's
# waitForURL(partitionURL) polls .../ledger-meta/FFFFFFFF--0-63999/
# and gets HTTP 404 forever (unbounded for{} poll, no internal
# deadline) until the wrapper timeout fires (exit 124). This is an
# upstream IMAGE-VERSION incompatibility, NOT a henyey-correctness
# break — tracked in #3565 — so this shard mirrors the testnet
# shard's de-gate: soft_on_timeout converts a TIMEOUT (exit
# 124/137) into a neutral SOFT-SKIP (exit 0, grep-able marker)
# while any genuine assertion failure (non-timeout exit 1) STILL
# stays RED. probe_timeout: 240 + step_timeout_minutes: 25 make the
# soft-skip fire in minutes so the PR's own CI never reproduces the
# ~55-min hang. The soft-skip is TIMEOUT-ONLY and TEMPORARY: remove
# it once the galexie image is bumped to a Protocol-27-capable tag
# (deliberately OUT OF SCOPE here — bumping a still-incompatible tag
# would re-hang CI; see #3565).
- network: local
enable: galexie
probes: "test_galexie.go"
soft_on_timeout: true
probe_timeout: 240
step_timeout_minutes: 25
# Additional testnet shard.
#
# De-gate `main` on testnet external-liveness (#3272): the testnet
# shard flakes on external network health (slow catchup, checkpoint
# cadence, archive availability) — NOT henyey correctness — and a
# stuck sync probe TIMEOUT used to red-roll the whole workflow and
# block every PR's merge gate. So this shard:
# * sets soft_on_timeout: true — a probe TIMEOUT (exit 124) is
# converted to a neutral soft-skip by run-quickstart-test.sh
# (a genuine assertion failure, non-124, still stays red), and
# * tightens probe_timeout to 240s (= timeout_multiplier(4) * 60,
# the upstream 4-min precedent already documented above) so a
# stuck probe fast-fails into the soft-skip in minutes with
# diagnostics uploaded, instead of hanging until the 45-min
# job wall-clock cancels the run.
# This extends the existing testnet-RPC-disabled precedent (see the
# "Testnet shard (#1848)" note above) to the sync-probe TIMEOUT
# outcome. Scoped to THIS shard only — local/pubnet keep the default
# 600s hard-fail-on-timeout and carry no soft flag.
# Budget: 4 probes × 240s + one 240s retry = 1200s (~20 min), well
# under the job's 45-min timeout-minutes.
#
# DIAGNOSTIC instrumentation (#3286): this shard also sets a TIGHT,
# testnet-only step-level fail-fast bound via step_timeout_minutes: 25
# (applied as the "Run probes through wrapper" step's timeout-minutes
# below). Two wrapper-level fixes (#3273, #3287) both FAILED to bound
# the ~55-min step-8 hang, and a 45-min JOB-level wall-clock is a
# *cancel* — so `if: failure()` never fires and the diagnostics step
# never uploads. A step-level timeout-minutes kill instead marks the
# STEP failed (distinct from a job cancel), so the upload step runs.
# 25 min > the ~20-min healthy budget above, so a healthy run never
# trips it. Other shards inherit the generous 360-min default (a
# no-op under the job's own 45-min cap) and stay byte-identical.
- network: testnet
enable: "core,horizon"
probes: "test_core.go test_horizon_up.go test_horizon_core_up.go test_horizon_ingesting.go"
soft_on_timeout: true
probe_timeout: 240
step_timeout_minutes: 25
# Additional pubnet shard
# Note: upstream excludes horizon_core_up, horizon_ingesting, and
# stellar_rpc_healthy on pubnet (see internal-test.yml conditionals).
- network: pubnet
enable: "core,rpc,horizon"
probes: "test_core.go test_horizon_up.go test_stellar_rpc_up.go"
steps:
- uses: actions/checkout@v4
- name: Download quickstart image
uses: actions/download-artifact@v4
with:
name: image-quickstart-testing-with-pr-amd64.tar
path: /tmp/quickstart-image
- name: Load Docker image
run: |
tarball="$(ls /tmp/quickstart-image/image 2>/dev/null || ls /tmp/quickstart-image/*.tar 2>/dev/null | head -1)"
docker load -i "$tarball"
- name: Checkout stellar/quickstart (tests)
uses: actions/checkout@v4
with:
repository: stellar/quickstart
ref: ${{ needs.setup.outputs.quickstart-sha }}
path: quickstart
- uses: actions/setup-go@v5
with:
go-version: 'stable'
cache: false
- name: Run quickstart container
run: |
docker run -d --name quickstart \
-p 8000:8000 -p 11626:11626 -p 8001:8001 \
quickstart:testing-with-pr-amd64 \
--${{ matrix.network }} \
--enable ${{ matrix.enable }}
- name: Run probes through wrapper
# DIAGNOSTIC fail-fast bound (#3286). Testnet sets
# matrix.step_timeout_minutes: 25 (tighter than its ~20-min healthy
# budget) so a hang here FAILS the STEP at ~25 min instead of running
# to the 45-min JOB cancel — a step-timeout marks the step *failed*, so
# the "Upload diagnostics" step (if: failure()) actually runs and the
# watchdog's process-tree dump is uploaded. All other shards have no
# step_timeout_minutes set and inherit the generous 360-min default
# (a no-op under the job's 45-min cap), so they stay byte-identical.
timeout-minutes: ${{ matrix.step_timeout_minutes || 360 }}
env:
NETWORK: ${{ matrix.network }}
ENABLE: ${{ matrix.enable }}
# Per-shard overrides (#3272). Unset matrix keys render as empty
# strings; the loop below falls back to the default budget and omits
# the soft flag when these are empty. Only the testnet shard sets them.
SHARD_PROBE_TIMEOUT: ${{ matrix.probe_timeout }}
SHARD_SOFT_ON_TIMEOUT: ${{ matrix.soft_on_timeout }}
run: |
# Per-probe budget = github.run_attempt * timeout_multiplier minutes,
# converted to seconds for GNU `timeout` (* 60). Mirrors upstream
# internal-test.yml's `minutes = github.run_attempt * timeout_multiplier`
# applied as each probe step's timeout-minutes (attempt 1 -> 4 min;
# manual re-runs escalate to 8, 12, ...). github.run_attempt is constant
# across a job's steps, so computing it once here is equivalent to
# upstream's per-step timeout-minutes. The budget is applied per-probe;
# a shard running N probes can use up to N * budget wall-clock, bounded
# overall by this job's timeout-minutes: 45. The targeted single retry
# in run-quickstart-test.sh is layered on top of this budget (#2920).
PROBE_TIMEOUT=$(( ${{ github.run_attempt }} * timeout_multiplier * 60 ))
# Per-shard probe-timeout override (#3272). The testnet shard fast-
# fails at a tighter budget (SHARD_PROBE_TIMEOUT=240) so a stuck sync
# probe is soft-skipped in minutes; all other shards leave it empty and
# keep the upstream run_attempt * multiplier * 60 budget unchanged.
if [[ -n "$SHARD_PROBE_TIMEOUT" ]]; then
PROBE_TIMEOUT="$SHARD_PROBE_TIMEOUT"
fi
# Per-shard soft-on-timeout flag (#3272). Only the testnet shard sets
# matrix.soft_on_timeout: true; for every other shard SHARD_SOFT_ON_TIMEOUT
# is empty and SOFT_FLAG stays empty so the wrapper invocation is
# byte-identical to before (no --soft-on-timeout passed).
SOFT_FLAG=""
if [[ "$SHARD_SOFT_ON_TIMEOUT" == "true" ]]; then
SOFT_FLAG="--soft-on-timeout"
fi
DIAG_DIR="/tmp/quickstart-diagnostics"
# ---- DIAGNOSTIC hang watchdog (#3286), testnet shard only ----
# Capture the step shell's PID HERE, in the step body — NOT inside a
# subshell and NOT via $BASHPID — so the backgrounded watchdog can
# dump the fds THIS shell holds. The watchdog sleeps WATCHDOG_DELAY
# (default 1200s = 20 min, < the 25-min step_timeout_minutes bound)
# then writes a process-tree + open-fd snapshot to the diagnostics
# dir, so on the next hang the dump is on disk BEFORE the step kill
# and gets swept into the uploaded artifact. It is reaped on the
# healthy path (trap EXIT + explicit kill after the loop) so it never
# lingers or emits a spurious dump on a passing run. This does NOT fix
# the hang — its deliverable is DATA from the next hang.
STEP_PID=$$
WATCHDOG_PID=""
if [[ "$NETWORK" == "testnet" ]]; then
( sleep "${WATCHDOG_DELAY:-1200}"; \
bash scripts/ci/quickstart-hang-watchdog.sh \
"$DIAG_DIR/testnet-hang-watchdog" "$STEP_PID" ) &
WATCHDOG_PID=$!
# Reap the watchdog on any step exit so a passing run never leaks
# the backgrounded sleep.
trap '[[ -n "$WATCHDOG_PID" ]] && kill "$WATCHDOG_PID" 2>/dev/null || true' EXIT
fi
for probe_file in ${{ matrix.probes }}; do
probe_name="${probe_file%.go}"
probe_name="${probe_name#test_}"
# Normalize underscores to hyphens so probe names match the
# retry policy (e.g. horizon_core_up → horizon-core-up).
probe_name="${probe_name//_/-}"
scripts/ci/run-quickstart-test.sh \
$SOFT_FLAG \
--network "$NETWORK" \
--enable "$ENABLE" \
--probe "$probe_name" \
--timeout "$PROBE_TIMEOUT" \
--diagnostics-dir "$DIAG_DIR/$NETWORK-$ENABLE-$probe_name" \
-- go run "quickstart/tests/$probe_file"
done
# Healthy path: probes finished, so reap the watchdog now (explicit
# kill, belt-and-suspenders with the EXIT trap) — no lingering sleep,
# no spurious dump on a passing run.
if [[ -n "$WATCHDOG_PID" ]]; then
kill "$WATCHDOG_PID" 2>/dev/null || true
fi
- name: Upload diagnostics on failure
# DIAGNOSTIC instrumentation (#3286): upload on BOTH a step-timeout
# failure (the primary path — a testnet step_timeout_minutes kill marks
# the step failed, so failure() fires) AND a residual job-level cancel
# (belt-and-suspenders). The watchdog writes its dump before the step
# kill, so the artifact exists for either disposition.
if: ${{ failure() || cancelled() }}
uses: actions/upload-artifact@v4
with:
name: quickstart-diagnostics-${{ matrix.network }}-${{ matrix.enable }}
path: /tmp/quickstart-diagnostics/
retention-days: 7
- name: Cleanup
if: always()
run: |
docker stop quickstart 2>/dev/null || true
docker rm quickstart 2>/dev/null || true
# NOTE (#3193): the previous in-run `rerun-on-transient` job was REMOVED. A job
# inside a run cannot re-dispatch its own still-running run — the rerun-failed
# API call returns "This workflow is already running" (exit 1), so the
# auto-retry never fired and only added a spurious FAILURE check. Recovery from
# a whole-runner reclamation now lives in .github/workflows/quickstart-retry.yml,
# a separate workflow_run-triggered workflow that fires AFTER this run
# completes (so the re-dispatch is accepted).