Skip to content

Commit 1a4c089

Browse files
authored
fix(ci): unblock Vercel deploys (Hobby cron limit) and the contracts CI job (#312)
2 parents 812fce9 + 6329d48 commit 1a4c089

39 files changed

Lines changed: 15085 additions & 76 deletions

File tree

.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ NEXT_PUBLIC_ORACLE_ADDRESS=
140140
ORACLE_SECRET_KEY=
141141

142142
# ── Liquidation Keeper (scripts/liquidation-keeper.ts) ────────────────────────
143-
# Automated bot that liquidates under-collateralized loans. Deployed as a
144-
# background worker that monitors every minute: vercel.json schedules
145-
# POST /api/cron/liquidation on "* * * * *" (authenticated with Bearer CRON_SECRET,
146-
# same as the other crons) — or run `npm run liquidation:keeper:service`
143+
# Automated bot that liquidates under-collateralized loans. Vercel Hobby only
144+
# permits daily crons, so vercel.json runs POST /api/cron/liquidation once a
145+
# day as a safety net and .github/workflows/keepers.yml hits the same endpoint
146+
# every 5 minutes (needs the KEEPER_BASE_URL + CRON_SECRET repo secrets) — or
147+
# run `npm run liquidation:keeper:service`
147148
# (--interval=60) self-hosted. One-shot `npm run liquidation:keeper` remains
148149
# available for cron schedulers. Requires ADMIN_SECRET_KEY (above) to sign
149150
# liquidation transactions, plus NEXT_PUBLIC_LENDING_CONTRACT_ID /

.github/workflows/keepers.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Keepers (liquidation + price oracle)
2+
3+
# Vercel Hobby only allows one cron run per day, which is far too slow for the
4+
# liquidation keeper and the collateral price oracle. GitHub Actions can fire
5+
# every 5 minutes, so it pings the same authenticated cron endpoints instead.
6+
#
7+
# Required repository secrets:
8+
# KEEPER_BASE_URL e.g. https://trustlend-stellar.vercel.app (no trailing slash)
9+
# CRON_SECRET must match the CRON_SECRET env var configured on Vercel
10+
#
11+
# The workflow is a no-op until both secrets exist, so it is safe on forks.
12+
13+
on:
14+
schedule:
15+
- cron: "*/5 * * * *"
16+
workflow_dispatch:
17+
18+
concurrency:
19+
group: keepers
20+
cancel-in-progress: false
21+
22+
jobs:
23+
run:
24+
name: Trigger cron endpoints
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 5
27+
env:
28+
KEEPER_BASE_URL: ${{ secrets.KEEPER_BASE_URL }}
29+
CRON_SECRET: ${{ secrets.CRON_SECRET }}
30+
steps:
31+
- name: Skip when secrets are not configured
32+
id: gate
33+
run: |
34+
if [ -z "$KEEPER_BASE_URL" ] || [ -z "$CRON_SECRET" ]; then
35+
echo "KEEPER_BASE_URL / CRON_SECRET not set — nothing to do."
36+
echo "configured=false" >> "$GITHUB_OUTPUT"
37+
else
38+
echo "configured=true" >> "$GITHUB_OUTPUT"
39+
fi
40+
41+
- name: Liquidation keeper
42+
if: steps.gate.outputs.configured == 'true'
43+
run: |
44+
curl --fail-with-body --silent --show-error --max-time 120 \
45+
-X POST "$KEEPER_BASE_URL/api/cron/liquidation" \
46+
-H "Authorization: Bearer $CRON_SECRET"
47+
48+
- name: Price oracle
49+
if: steps.gate.outputs.configured == 'true'
50+
run: |
51+
curl --fail-with-body --silent --show-error --max-time 120 \
52+
-X POST "$KEEPER_BASE_URL/api/cron/price-oracle" \
53+
-H "Authorization: Bearer $CRON_SECRET"

__tests__/api/cron/liquidation.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,25 @@ describe("POST /api/cron/liquidation", () => {
118118
});
119119
});
120120

121-
// ── Acceptance criterion: the worker monitors prices every minute ──────────────
122-
// vercel.json must schedule the liquidation cron on a 1-minute cadence.
121+
// ── Scheduling ─────────────────────────────────────────────────────────────────
122+
// Vercel Hobby rejects any cron that runs more than once a day, so vercel.json
123+
// keeps a daily safety-net schedule and the 5-minute cadence lives in
124+
// .github/workflows/keepers.yml. Both must keep pointing at this route.
123125

124-
describe("vercel.json liquidation schedule", () => {
125-
it("schedules /api/cron/liquidation every minute (* * * * *)", () => {
126+
describe("liquidation cron scheduling", () => {
127+
it("vercel.json schedules /api/cron/liquidation once a day (Hobby-compatible)", () => {
126128
const raw = fs.readFileSync(path.resolve(process.cwd(), "vercel.json"), "utf8");
127129
const crons = (JSON.parse(raw) as { crons: Array<{ path: string; schedule: string }> }).crons;
128130

129131
const liquidationCron = crons.find((c) => c.path === "/api/cron/liquidation");
130132
expect(liquidationCron).toBeDefined();
131-
expect(liquidationCron?.schedule).toBe("* * * * *");
133+
// "m h * * *" — exactly one run per day
134+
expect(liquidationCron?.schedule).toMatch(/^\d{1,2} \d{1,2} \* \* \*$/);
135+
});
136+
137+
it("the GitHub Actions keeper workflow triggers the route every 5 minutes", () => {
138+
const raw = fs.readFileSync(path.resolve(process.cwd(), ".github/workflows/keepers.yml"), "utf8");
139+
expect(raw).toContain(`cron: "*/5 * * * *"`);
140+
expect(raw).toContain("/api/cron/liquidation");
132141
});
133142
});

app/api/cron/liquidation/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { loadConfig, runLiquidationKeeper } from "@/scripts/liquidation-keeper";
55
/**
66
* POST/GET /api/cron/liquidation
77
*
8-
* Automated Liquidation Bot (issue #259). Triggered by Vercel Cron every minute
9-
* (`vercel.json`) or any external scheduler (GitHub Actions, cURL, systemd).
8+
* Automated Liquidation Bot (issue #259). Triggered every 5 minutes by
9+
* `.github/workflows/keepers.yml`, once a day by Vercel Cron (`vercel.json`,
10+
* the Hobby-plan ceiling) or any external scheduler (cURL, systemd).
1011
* Loads the keeper configuration from env, scans open loans for
1112
* under-collateralization, and automatically submits `mark_defaulted` for any
1213
* loan whose LTV has crossed the contract's dynamic liquidation threshold.

contracts/governance/test_snapshots/test/test_double_vote_panics.1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -881,18 +881,18 @@
881881
},
882882
{
883883
"key": {
884-
"symbol": "freeze_reason"
884+
"symbol": "flags"
885885
},
886886
"val": {
887-
"string": ""
887+
"u32": 2
888888
}
889889
},
890890
{
891891
"key": {
892-
"symbol": "is_frozen"
892+
"symbol": "freeze_reason"
893893
},
894894
"val": {
895-
"bool": false
895+
"string": ""
896896
}
897897
},
898898
{

contracts/governance/test_snapshots/test/test_execute_requires_passed.1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -856,18 +856,18 @@
856856
},
857857
{
858858
"key": {
859-
"symbol": "freeze_reason"
859+
"symbol": "flags"
860860
},
861861
"val": {
862-
"string": ""
862+
"u32": 2
863863
}
864864
},
865865
{
866866
"key": {
867-
"symbol": "is_frozen"
867+
"symbol": "freeze_reason"
868868
},
869869
"val": {
870-
"bool": false
870+
"string": ""
871871
}
872872
},
873873
{

contracts/governance/test_snapshots/test/test_full_fee_change_flow.1.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,18 +1055,18 @@
10551055
},
10561056
{
10571057
"key": {
1058-
"symbol": "freeze_reason"
1058+
"symbol": "flags"
10591059
},
10601060
"val": {
1061-
"string": ""
1061+
"u32": 2
10621062
}
10631063
},
10641064
{
10651065
"key": {
1066-
"symbol": "is_frozen"
1066+
"symbol": "freeze_reason"
10671067
},
10681068
"val": {
1069-
"bool": false
1069+
"string": ""
10701070
}
10711071
},
10721072
{
@@ -1194,18 +1194,18 @@
11941194
},
11951195
{
11961196
"key": {
1197-
"symbol": "freeze_reason"
1197+
"symbol": "flags"
11981198
},
11991199
"val": {
1200-
"string": ""
1200+
"u32": 2
12011201
}
12021202
},
12031203
{
12041204
"key": {
1205-
"symbol": "is_frozen"
1205+
"symbol": "freeze_reason"
12061206
},
12071207
"val": {
1208-
"bool": false
1208+
"string": ""
12091209
}
12101210
},
12111211
{

contracts/governance/test_snapshots/test/test_propose_above_cap_rejected.1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -834,18 +834,18 @@
834834
},
835835
{
836836
"key": {
837-
"symbol": "freeze_reason"
837+
"symbol": "flags"
838838
},
839839
"val": {
840-
"string": ""
840+
"u32": 2
841841
}
842842
},
843843
{
844844
"key": {
845-
"symbol": "is_frozen"
845+
"symbol": "freeze_reason"
846846
},
847847
"val": {
848-
"bool": false
848+
"string": ""
849849
}
850850
},
851851
{

contracts/governance/test_snapshots/test/test_rejected_when_against_wins.1.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,18 +1050,18 @@
10501050
},
10511051
{
10521052
"key": {
1053-
"symbol": "freeze_reason"
1053+
"symbol": "flags"
10541054
},
10551055
"val": {
1056-
"string": ""
1056+
"u32": 2
10571057
}
10581058
},
10591059
{
10601060
"key": {
1061-
"symbol": "is_frozen"
1061+
"symbol": "freeze_reason"
10621062
},
10631063
"val": {
1064-
"bool": false
1064+
"string": ""
10651065
}
10661066
},
10671067
{
@@ -1189,18 +1189,18 @@
11891189
},
11901190
{
11911191
"key": {
1192-
"symbol": "freeze_reason"
1192+
"symbol": "flags"
11931193
},
11941194
"val": {
1195-
"string": ""
1195+
"u32": 2
11961196
}
11971197
},
11981198
{
11991199
"key": {
1200-
"symbol": "is_frozen"
1200+
"symbol": "freeze_reason"
12011201
},
12021202
"val": {
1203-
"bool": false
1203+
"string": ""
12041204
}
12051205
},
12061206
{

contracts/governance/test_snapshots/test/test_rejected_when_quorum_not_met.1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,18 +323,18 @@
323323
},
324324
{
325325
"key": {
326-
"symbol": "freeze_reason"
326+
"symbol": "flags"
327327
},
328328
"val": {
329-
"string": ""
329+
"u32": 2
330330
}
331331
},
332332
{
333333
"key": {
334-
"symbol": "is_frozen"
334+
"symbol": "freeze_reason"
335335
},
336336
"val": {
337-
"bool": false
337+
"string": ""
338338
}
339339
},
340340
{

0 commit comments

Comments
 (0)