Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
b5e6ed6
Rewrite pubnet parallel catchup v2 as Job-per-range
Jonathan-Eid Jul 30, 2026
0280156
Sum txApply and seconds across the resumed attempt chain
Jonathan-Eid Jul 30, 2026
d1b4ada
Drop the dead Prometheus test helper, pin the retry taxonomy
Jonathan-Eid Jul 30, 2026
cfc69d4
Remove dead cpu-sizing code; close mutation-sweep coverage gaps
Jonathan-Eid Jul 30, 2026
4ecc93e
Record attempt duration in the collector, as a fallback for reaped pods
Jonathan-Eid Jul 30, 2026
43547e8
Stop a worker's own output from killing its log stream
Jonathan-Eid Jul 30, 2026
08980ca
Gate the retry-path reap; cut the per-stream line buffer
Jonathan-Eid Jul 30, 2026
c0bd891
Collect logs by polling instead of one follow stream per pod
Jonathan-Eid Jul 30, 2026
2dffc45
Poll only pods whose log endpoint can answer
Jonathan-Eid Jul 30, 2026
1635311
Do not reap a succeeded Job until its record is actually complete
Jonathan-Eid Jul 30, 2026
fd8b099
Gate the reap on an explicit done marker from the collector
Jonathan-Eid Jul 30, 2026
5bd41f6
Wake a poller the moment its pod goes terminal or vanishes
Jonathan-Eid Jul 30, 2026
e95f104
Keep a ceiling-hit attempt's peak across a fresh start
Jonathan-Eid Jul 30, 2026
16d9067
Put the attempt number on the worker pod, not just the Job
Jonathan-Eid Jul 30, 2026
38ad01c
Never cpu-throttle a worker; escalate memory per OOM, not per attempt
Jonathan-Eid Jul 30, 2026
83e9b61
Treat a range already at its target as done, not as work to redo
Jonathan-Eid Jul 30, 2026
c17296c
Fix the offline-info LCL extraction; it was silently reading nothing
Jonathan-Eid Jul 30, 2026
07469cf
Do not report a duration for a pod the collector never watched run
Jonathan-Eid Jul 30, 2026
c15689f
Never let a peak on disk be lowered by a later write
Jonathan-Eid Jul 30, 2026
2b475ee
Read a pod's own start->finish instead of how long the poller watched
Jonathan-Eid Jul 30, 2026
4e08897
Raise the OOM retry budget to 10
Jonathan-Eid Jul 30, 2026
fca8180
Keep the OOM budget at 5, and say why
Jonathan-Eid Jul 30, 2026
945efdc
Never condemn a range on exit 3; it means "interrupted" as often as "…
Jonathan-Eid Jul 30, 2026
dcbce73
Project peakAnonBytes and wallSeconds into the mission's profile arti…
Jonathan-Eid Jul 30, 2026
7038644
Drain the run before failing on a condemned range
Jonathan-Eid Jul 30, 2026
b09e5f6
A condemned range must not freeze dispatch
Jonathan-Eid Jul 30, 2026
eb3c677
Behavioral test suite: a fake Kubernetes cluster, and fixes for 8 races
Jonathan-Eid Jul 30, 2026
e30ee14
Statelessness suite, and six defects it found
Jonathan-Eid Jul 30, 2026
dded200
Move the suite into tests/, grouped by what it exercises
Jonathan-Eid Jul 30, 2026
0b42421
Delete the monotonic-progress guard rather than persist it
Jonathan-Eid Jul 30, 2026
d4fd259
Re-read measurements from the volume when the mirror is all that is left
Jonathan-Eid Jul 30, 2026
93cae0e
Wire avoidNodeLabels through to the worker pods again
Jonathan-Eid Jul 30, 2026
1422824
Retire test_job_monitor.py for tests that run the code
Jonathan-Eid Jul 30, 2026
15ca968
Add longest-first dispatch, ordered by the profile rather than by pos…
Jonathan-Eid Jul 30, 2026
b840544
Size the cpu request from a range's slack, not from its demand
Jonathan-Eid Jul 30, 2026
e17cfc6
Rename profile_floor to longest_range_seconds
Jonathan-Eid Jul 30, 2026
d72e219
Key the cpu tier on rank, not on absolute seconds
Jonathan-Eid Jul 30, 2026
9856687
Collapse the cpu ladder into one setting
Jonathan-Eid Jul 30, 2026
afd80ac
Improve worker liveness metrics
Jonathan-Eid Jul 30, 2026
0885850
Pass mission namespace to Helm
Jonathan-Eid Jul 30, 2026
e68a0f9
Add runtime-weighted memory insurance
Jonathan-Eid Jul 30, 2026
56b4463
Fix retry counter reconstruction
Jonathan-Eid Jul 30, 2026
7967edd
Count unique disruption-retried ranges
Jonathan-Eid Jul 30, 2026
acfcde8
Extend parallel catchup attempt deadline
Jonathan-Eid Jul 30, 2026
3500dac
Pass --namespace to every helm and kubectl call
Jonathan-Eid Jul 30, 2026
76e1a40
Bound an attempt by its own profiled cost, not by one global number
Jonathan-Eid Jul 30, 2026
f68df05
Fix durable resume detection in collector
Jonathan-Eid Jul 30, 2026
6e74f10
Reconstruct resumed catchup profiles
Jonathan-Eid Jul 30, 2026
01b0a42
Harden restart-safe profile finalization
Jonathan-Eid Jul 30, 2026
71dc1ae
Omit unverifiable chain aggregates
Jonathan-Eid Jul 30, 2026
450729a
A range that reaches its deadline fails the mission
Jonathan-Eid Jul 30, 2026
6305366
Add synthetic resume restart harness
Jonathan-Eid Jul 30, 2026
106a563
Port the live monitor's liveness sampler and runtime memory insurance
Jonathan-Eid Jul 30, 2026
483001d
Harden synthetic harness startup
Jonathan-Eid Jul 30, 2026
e3ebbdd
Capture synthetic monitor startup failures
Jonathan-Eid Jul 30, 2026
b57c70b
Collapse the eight tmp+rename blocks into one _write_atomic
Jonathan-Eid Jul 30, 2026
1ed7e82
Stop condemning a range whose pod terminated with no exit code
Jonathan-Eid Jul 30, 2026
f8bf628
Make the attempt deadline a flat 12h and drop the dead knobs around it
Jonathan-Eid Jul 31, 2026
8b36059
Stop setting cpu and memory limits on workers
Jonathan-Eid Jul 31, 2026
44a576b
Merge resilience work from jonathan-eid-fix-resume-profiling
Jonathan-Eid Jul 31, 2026
534dad6
Fix duplicate liveness env that the merge left in the Deployment
Jonathan-Eid Jul 31, 2026
25e5371
Ship cpu tiering on by default
Jonathan-Eid Jul 31, 2026
7389aaa
Widen the cpu tier ladder to 7 bands topping at 2.0 cores
Jonathan-Eid Jul 31, 2026
38ae7b9
Drop duplicated liveness code, stop giving no-seconds ranges the top …
Jonathan-Eid Jul 31, 2026
08e5b4e
Stop V2 sizing the shared spec that V1 catchup also uses
Jonathan-Eid Jul 31, 2026
55fe6f2
Widen the cpu ladder to 8 bands topping at 3.0 cores
Jonathan-Eid Aug 1, 2026
8553e77
Restructure the monitor and make pooled on-demand runs schedulable
Jonathan-Eid Aug 7, 2026
97fc01a
catchup: size spot nebula for 1800m/9Gi on half-sized nodes
Jonathan-Eid Aug 7, 2026
8be7d87
Move the driver onto the monitor's HTTP surface
Jonathan-Eid Aug 8, 2026
1913623
Validate the whole config at /start, not at import
Jonathan-Eid Aug 8, 2026
c809215
Move the range into /start and pull logs per file
Jonathan-Eid Aug 8, 2026
40434b1
Fix two faults the first HTTP run surfaced
Jonathan-Eid Aug 8, 2026
f6b5e3e
Keep the collector's resume cursor out of the manifest
Jonathan-Eid Aug 8, 2026
63056dc
Drop the ConfigMap plumbing the HTTP surface replaced
Jonathan-Eid Aug 9, 2026
f192eb2
Open the reconcile gate where the run is installed, not in the POST h…
Jonathan-Eid Aug 9, 2026
967f0a4
Say in /status whether the run has started
Jonathan-Eid Aug 9, 2026
ed82235
catchup: make the job monitor image settable per run
Jonathan-Eid Aug 10, 2026
abfc682
Pin the dev image to 2026-08-09a
Jonathan-Eid Aug 10, 2026
f55038d
Delete the logarithmic range generator
Jonathan-Eid Aug 10, 2026
21a0dfd
Keep the parallel-catchup test suite local
Jonathan-Eid Aug 10, 2026
277632f
catchup: gate all chart RBAC behind monitor.createRbac
Jonathan-Eid Aug 10, 2026
a0cec3c
Add a worker memory-request override and delete dead PCv2 code
Jonathan-Eid Aug 10, 2026
39a9f01
Decide cache-bump rungs from a block list, not from node vCPU
Jonathan-Eid Aug 10, 2026
ad2a9fd
Delete the orphaned cpu-tiering prose from values.yaml
Jonathan-Eid Aug 10, 2026
908c47b
Delete the volume-spread constraint
Jonathan-Eid Aug 10, 2026
e4bae21
Take capacity from a node-label list, not a values overlay
Jonathan-Eid Aug 10, 2026
df7a200
Move the ParallelCatchupV2 tests into CatchupV2Tests.fs
Jonathan-Eid Aug 10, 2026
0fa75f4
Cut eight redundant V2 tests, add --pubnet-parallel-catchup-create-rbac
Jonathan-Eid Aug 10, 2026
d3e191e
Remove HANDOFF.md
Jonathan-Eid Aug 10, 2026
7e12140
Cut the field-projection test
Jonathan-Eid Aug 10, 2026
f496adc
Cut the volume-peak test
Jonathan-Eid Aug 10, 2026
daacac9
Stop sizing ephemeral storage at all in pvc mode
Jonathan-Eid Aug 10, 2026
f59bd49
Drop the two size-only assertions from the profile test
Jonathan-Eid Aug 10, 2026
15f9c93
Name the profile guard test after what it actually prevents
Jonathan-Eid Aug 10, 2026
8ef9814
Cover the mixed case: some ranges measured, some not
Jonathan-Eid Aug 10, 2026
54ac412
Tighten the comments in CatchupV2Tests
Jonathan-Eid Aug 10, 2026
7a633af
Fold the V2 tests back into Tests.fs
Jonathan-Eid Aug 10, 2026
d3d4997
Stop passing the chart its own values file
Jonathan-Eid Aug 10, 2026
f106b00
Make the values file overridable with SUPERCLUSTER_VALUES_PATH
Jonathan-Eid Aug 10, 2026
bde2160
Accept an http profile URL, and cover every resolution path
Jonathan-Eid Aug 10, 2026
5031d1a
Drop the profile-resolution tests
Jonathan-Eid Aug 10, 2026
530644f
Log the monitor process to one file, named for the process
Jonathan-Eid Aug 11, 2026
b79a883
Sort collected artifacts into folders, and summarise pods by phase
Jonathan-Eid Aug 11, 2026
b1a6e18
Merge branch 'main' into jonathan/catchup-k8s-job
Jonathan-Eid Aug 11, 2026
746b2be
Print status and pod lines every ten minutes
Jonathan-Eid Aug 11, 2026
8c8d5b8
Refetch rewritten artifacts whole instead of resuming them
Jonathan-Eid Aug 11, 2026
32e8ed1
Always pull the job monitor image
Jonathan-Eid Aug 11, 2026
9a07a28
Delete the leftover CAPACITY_TYPE env
Jonathan-Eid Aug 11, 2026
07aa5a5
Let a run choose where the job monitor lands
Jonathan-Eid Aug 11, 2026
fd49f9c
Stop values.yaml re-deriving what lib/config.py already establishes
Jonathan-Eid Aug 11, 2026
3029b80
Run fantomas, and cap values.yaml comments at three sentences
Jonathan-Eid Aug 11, 2026
950cc59
Merge branch 'main' into jonathan/catchup-k8s-job
Jonathan-Eid Aug 11, 2026
07616f7
Put main's new tests back inside the Tests class
Jonathan-Eid Aug 11, 2026
3b8ec9c
refactor(parallel-catchup): file lib/ by owner, split config three ways
Jonathan-Eid Aug 14, 2026
a453350
refactor(parallel-catchup): rewrite the collector as one cycle over t…
Jonathan-Eid Aug 14, 2026
c0f88c4
Replace the parallel-catchup job monitor with the v2 rewrite
Jonathan-Eid Aug 17, 2026
3c78b25
Settle a failed attempt the collector never opened
Jonathan-Eid Aug 17, 2026
ef3e8f3
Split monitor config out of the shared module, and de-duplicate the p…
Jonathan-Eid Aug 17, 2026
ccfaa20
Step the resumed chain over an attempt that ran nothing
Jonathan-Eid Aug 17, 2026
26e7e66
Claim an attempt while its pod is still Pending
Jonathan-Eid Aug 18, 2026
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@
.fake
.ionide
.idea

# Python bytecode from the parallel-catchup monitor/collector tests
__pycache__/
*.pyc

# pytest-cov artifacts from the MissionParallelCatchup suite
.coverage
**/__pycache__/
103 changes: 103 additions & 0 deletions src/App/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,20 @@ type MissionOptions
pubnetParallelCatchupStartingLedger: int,
pubnetParallelCatchupEndLedger: int option,
pubnetParallelCatchupLedgersPerJob: int,
pubnetParallelCatchupOverlapLedgers: int,
pubnetParallelCatchupNumWorkers: int,
pubnetParallelCatchupStorageMode: string,
pubnetParallelCatchupProfile: string,
pubnetParallelCatchupRangeOrder: string,
pubnetParallelCatchupPoolPrefix: string,
jobMonitorImagePcV2: string,
pubnetParallelCatchupCpuRequest: string,
pubnetParallelCatchupMemRequest: string,
pubnetParallelCatchupPoolCpu: string,
pubnetParallelCatchupPoolMem: string,
pubnetParallelCatchupCreateRbac: bool,
jobMonitorNodeLabels: seq<string>,
jobMonitorTolerateTaints: seq<string>,
tag: string option,
numPregeneratedTxs: int option,
genesisTestAccountCount: int option,
Expand Down Expand Up @@ -515,12 +528,88 @@ type MissionOptions
Default = 16000)>]
member self.PubnetParallelCatchupLedgersPerJob = pubnetParallelCatchupLedgersPerJob

[<Option("pubnet-parallel-catchup-overlap-ledgers",
HelpText = "ledgers each job replays before its own range, so a range is ledgersPerJob + this. Not a tuning knob: a profile measured at one overlap does not describe a run at another, and 0 measures a job that skips the buckets the next one needs (only supported for V2)",
Required = false,
Default = 320)>]
member self.PubnetParallelCatchupOverlapLedgers = pubnetParallelCatchupOverlapLedgers

[<Option("pubnet-parallel-catchup-num-workers",
HelpText = "number of workers to run parallel catchup with (only supported for V2)",
Required = false,
Default = 192)>]
member self.PubnetParallelCatchupNumWorkers = pubnetParallelCatchupNumWorkers

[<Option("pubnet-parallel-catchup-storage-mode",
HelpText = "worker /data backing: 'pvc' keeps it across pods so an evicted range resumes at L+1 (needed for spot); 'ephemeral' puts it on the node disk where retries cannot resume (only supported for V2)",
Required = false,
Default = "pvc")>]
member self.PubnetParallelCatchupStorageMode : string = pubnetParallelCatchupStorageMode
Comment on lines +543 to +547

[<Option("pubnet-parallel-catchup-profile",
HelpText = "range profile from an earlier run, used to tighten per-range requests: a local path or an https URL to the JSON artifact. Only tightens requests; limits are unchanged (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupProfile : string = pubnetParallelCatchupProfile

[<Option("pubnet-parallel-catchup-range-order",
HelpText = "dispatch order: 'tip-first' (default) or 'oldest-first'. Generators emit tip-first, which front-loads the most expensive ranges; oldest-first profiles the cheap early ones first (only supported for V2)",
Required = false,
Default = "tip-first")>]
member self.PubnetParallelCatchupRangeOrder : string = pubnetParallelCatchupRangeOrder

[<Option("pubnet-parallel-catchup-pool-prefix",
HelpText = "route each range to a nodepool sized for its measured memory, e.g. 'catchup' selects catchup-dwarf, catchup-giant and so on. Each range gets its node to itself. Empty = one pool for every worker, the pre-tier behaviour (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupPoolPrefix : string = pubnetParallelCatchupPoolPrefix

[<Option("job-monitor-image-pc-v2",
HelpText = "Container image for the ParallelCatchupV2 job monitor and log collector. Empty uses the chart default.",
Required = false,
Default = "")>]
member self.JobMonitorImagePcV2 : string = jobMonitorImagePcV2

[<Option("pubnet-parallel-catchup-cpu-request",
HelpText = "override the worker cpu request for an UNPOOLED run, e.g. 1000m or 500m. Ignored once --pubnet-parallel-catchup-pool-prefix is set: there the tier's own cut is the request, because a flat value shipped at a tier whose nodes are smaller leaves the pod permanently Pending. Empty = the chart default (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupCpuRequest : string = pubnetParallelCatchupCpuRequest

[<Option("pubnet-parallel-catchup-mem-request",
HelpText = "override the worker memory request for an UNPOOLED run, e.g. 9Gi or 12Gi. Same scope as --pubnet-parallel-catchup-cpu-request: ignored under a pool prefix, where the tier's cut is the request. A range the profile has measured overrides it. Empty = the chart default, 9Gi (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupMemRequest : string = pubnetParallelCatchupMemRequest

[<Option("pubnet-parallel-catchup-pool-cpu",
HelpText = "per-tier cpu claim, \"tier:cores\" comma separated, e.g. \"dwarf:0.85,giant:1.85\". The right map depends on the capacity the run targets: spot pools were doubled so a claim is half a node and two pods share it, on-demand pools were not, and there the same claim is the node's nameplate, which is not allocatable and schedules nothing. Empty uses the chart default (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupPoolCpu : string = pubnetParallelCatchupPoolCpu

[<Option("pubnet-parallel-catchup-pool-mem",
HelpText = "per-tier memory claim, \"tier:quantity\" comma separated, e.g. \"dwarf:1280Mi,giant:6656Mi\". This is what isolates a pod on its node, so it is the dimension that sets pods-per-node. Same capacity caveat as --pubnet-parallel-catchup-pool-cpu. Empty uses the chart default (only supported for V2)",
Required = false,
Default = "")>]
member self.PubnetParallelCatchupPoolMem : string = pubnetParallelCatchupPoolMem

[<Option("pubnet-parallel-catchup-create-rbac",
HelpText = "have the chart create the monitor's Role, RoleBinding, ClusterRole and ClusterRoleBinding. Default false: on ssc-eks the namespace already provides them (a catchup-job-monitor RoleBinding covering system:serviceaccounts:stellar-supercluster), and creating them needs the installer to hold those rights. Set it on a cluster that has no such binding -- without it the monitor 403s on its first ConfigMap read and dispatches nothing (only supported for V2)",
Required = false,
Default = false)>]
member self.PubnetParallelCatchupCreateRbac : bool = pubnetParallelCatchupCreateRbac

[<Option("job-monitor-node-labels",
HelpText = "`key:value` labels the job monitor's node must carry, e.g. purpose:catchup-dwarf catchup-capacity:od. All of them must match. Empty puts it wherever it fits, which for a cluster whose catchup pools are tainted means the untainted shared nodes (only supported for V2)",
Required = false)>]
member self.JobMonitorNodeLabels = jobMonitorNodeLabels

[<Option("job-monitor-tolerate-taints",
HelpText = "taints the job monitor tolerates, `key` or `key:effect`, effect defaulting to NoSchedule. Needed to put it on a tainted pool -- --job-monitor-node-labels alone leaves it unschedulable there (only supported for V2)",
Required = false)>]
member self.JobMonitorTolerateTaints = jobMonitorTolerateTaints

[<Option("tag", HelpText = "optional name to tag the run with", Required = false)>]
member self.Tag = tag

Expand Down Expand Up @@ -897,7 +986,21 @@ let main argv =
pubnetParallelCatchupStartingLedger = mission.PubnetParallelCatchupStartingLedger
pubnetParallelCatchupEndLedger = mission.PubnetParallelCatchupEndLedger
pubnetParallelCatchupLedgersPerJob = mission.PubnetParallelCatchupLedgersPerJob
pubnetParallelCatchupOverlapLedgers = mission.PubnetParallelCatchupOverlapLedgers
pubnetParallelCatchupNumWorkers = mission.PubnetParallelCatchupNumWorkers
pubnetParallelCatchupStorageMode = mission.PubnetParallelCatchupStorageMode
pubnetParallelCatchupProfile = mission.PubnetParallelCatchupProfile
pubnetParallelCatchupRangeOrder = mission.PubnetParallelCatchupRangeOrder
pubnetParallelCatchupPoolPrefix = mission.PubnetParallelCatchupPoolPrefix
jobMonitorImagePcV2 = mission.JobMonitorImagePcV2
pubnetParallelCatchupCpuRequest = mission.PubnetParallelCatchupCpuRequest
pubnetParallelCatchupMemRequest = mission.PubnetParallelCatchupMemRequest
pubnetParallelCatchupPoolCpu = mission.PubnetParallelCatchupPoolCpu
pubnetParallelCatchupPoolMem = mission.PubnetParallelCatchupPoolMem
pubnetParallelCatchupCreateRbac = mission.PubnetParallelCatchupCreateRbac
jobMonitorNodeLabels = List.map splitLabel (List.ofSeq mission.JobMonitorNodeLabels)
jobMonitorTolerateTaints =
List.map splitLabel (List.ofSeq mission.JobMonitorTolerateTaints)
tag = mission.Tag
numPregeneratedTxs = mission.NumPregeneratedTxs
enableTailLogging = true
Expand Down
66 changes: 33 additions & 33 deletions src/FSLibrary.Tests/FSLibrary.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="stellar-dotnet-sdk" Version="10.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FSLibrary\FSLibrary.fsproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="stellar-dotnet-sdk" Version="10.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FSLibrary\FSLibrary.fsproj" />
</ItemGroup>

</Project>
Loading