Skip to content

Vault roll

Vault roll #460

Workflow file for this run

name: Vault roll
on:
schedule:
- cron: "*/20 * * * *"
workflow_dispatch:
concurrency:
group: vault-roll
cancel-in-progress: false
permissions:
contents: write
jobs:
roll:
name: Close settled cycle and open the next
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: keeper
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: keeper/package-lock.json
- name: Install keeper dependencies
run: npm ci
- name: Rebase latest receipts before rolling
run: git pull --rebase origin main
working-directory: ${{ github.workspace }}
- name: Validate Vercel deploy token before rolling
working-directory: ${{ github.workspace }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
node --input-type=module <<'NODE'
const token = process.env.VERCEL_TOKEN;
const teamId = process.env.VERCEL_ORG_ID;
const projectId = process.env.VERCEL_PROJECT_ID;
if (!token) throw new Error("VERCEL_TOKEN secret is required before vault roll");
if (!teamId) throw new Error("VERCEL_ORG_ID secret is required before vault roll");
if (!projectId) throw new Error("VERCEL_PROJECT_ID secret is required before vault roll");
const response = await fetch(`https://api.vercel.com/v10/projects/${projectId}?teamId=${teamId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const text = await response.text();
throw new Error(`VERCEL_TOKEN preflight failed with ${response.status}: ${text}`);
}
const project = await response.json();
console.log("CAPLETFI_VERCEL_TOKEN_VALID");
console.log(JSON.stringify({ project: project.name, projectId }, null, 2));
NODE
- name: Roll the vault
run: npx tsx src/vaultCycle.ts roll
env:
VAULT_OPERATOR_PRIVATE_KEY: ${{ secrets.VAULT_OPERATOR_PRIVATE_KEY }}
- name: Commit fill receipts
id: receipts
run: |
if [ -n "$(git status --porcelain data/vault_fills.ndjson)" ]; then
echo "changed=1" >> "$GITHUB_OUTPUT"
cp data/vault_fills.ndjson ../web/public/vault_fills.ndjson
cd ..
npm run verify:ledger
git config user.name qdee
git config user.email iyandaadedolapo@gmail.com
git add keeper/data/vault_fills.ndjson web/public/vault_fills.ndjson
# Do not use [skip ci]: Vercel treats it as a deploy skip, so live receipts go stale.
# The main CI workflow ignores receipt-only commits to keep scheduled rolls cheap.
git commit -m "receipts: vault roll fill ledger"
git pull --rebase origin main
git push origin main
else
echo "changed=0" >> "$GITHUB_OUTPUT"
fi
- name: Deploy fresh receipts to Vercel
if: steps.receipts.outputs.changed == '1'
working-directory: web
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
test -n "$VERCEL_ORG_ID" || { echo "VERCEL_ORG_ID secret is required for receipt freshness"; exit 1; }
test -n "$VERCEL_PROJECT_ID" || { echo "VERCEL_PROJECT_ID secret is required for receipt freshness"; exit 1; }
test -n "$VERCEL_TOKEN" || { echo "VERCEL_TOKEN secret is required for receipt freshness"; exit 1; }
npx --yes vercel@latest deploy --prod --yes --token="$VERCEL_TOKEN"
- name: Verify live receipt freshness
if: steps.receipts.outputs.changed == '1'
working-directory: ${{ github.workspace }}
run: |
node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
const rows = readFileSync("web/public/vault_fills.ndjson", "utf8")
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => JSON.parse(line));
const latest = rows.at(-1);
if (!latest?.tx) throw new Error("committed receipt ledger has no latest tx");
let lastLive = "";
for (let attempt = 1; attempt <= 20; attempt += 1) {
const response = await fetch("https://capletfi.vercel.app/api/vault/ledger", {
headers: { "cache-control": "no-cache" },
});
if (response.ok) {
const live = await response.json();
lastLive = live.summary?.latestTx ?? "";
if (lastLive === latest.tx) {
console.log("CAPLETFI_RECEIPT_DEPLOY_FRESH");
console.log(JSON.stringify({ latestTx: latest.tx, attempt }, null, 2));
process.exit(0);
}
}
await new Promise((resolve) => setTimeout(resolve, 15_000));
}
throw new Error(`live ledger latestTx ${lastLive || "(missing)"} did not match committed ${latest.tx}`);
NODE