Skip to content

CI/CD recipes

Everything here needs only curl, jq and sha256sum. For GitHub Actions, prefer the official action.

Store As
CAVS_TOKEN a masked, protected secret — a cavs_ci_ token with repo:write
CAVS_API a plain variable — https://cavsnode.com
CAVS_REPO_ID a plain variable — the repository UUID

Drop this in your repository and call it from any pipeline.

scripts/cavs.sh
#!/usr/bin/env bash
# Minimal CAVS Node helper: upload and download by content hash.
# Requires: curl, jq, sha256sum. Expects CAVS_API, CAVS_TOKEN, CAVS_REPO_ID.
set -euo pipefail
: "${CAVS_API:?set CAVS_API, e.g. https://cavsnode.com}"
: "${CAVS_TOKEN:?set CAVS_TOKEN}"
: "${CAVS_REPO_ID:?set CAVS_REPO_ID}"
API="${CAVS_API%/}/api/v1"
AUTH=(-H "Authorization: Bearer ${CAVS_TOKEN}")
JSON=(-H "Content-Type: application/json")
cavs_upload() { # cavs_upload <file> [idempotency-key]
local file="$1" key="${2:-upload-$(basename "$1")-$$}"
local oid size session url
oid=$(sha256sum "$file" | cut -d' ' -f1)
size=$(wc -c < "$file" | tr -d ' ')
session=$(curl -sS --fail-with-body -X POST "$API/repositories/$CAVS_REPO_ID/uploads" \
"${AUTH[@]}" "${JSON[@]}" -H "Idempotency-Key: $key" \
-d '{"expected_objects":1}' | jq -r '.session.id')
url=$(curl -sS --fail-with-body -X POST \
"$API/repositories/$CAVS_REPO_ID/uploads/$session/objects" \
"${AUTH[@]}" "${JSON[@]}" \
-d "{\"objects\":[{\"oid\":\"$oid\",\"size\":$size}]}" \
| jq -r '.objects[0].upload_url')
curl -sS --fail-with-body -X PUT "$url" --upload-file "$file"
curl -sS --fail-with-body -X POST \
"$API/repositories/$CAVS_REPO_ID/uploads/$session/objects/$oid/complete" \
"${AUTH[@]}" "${JSON[@]}" -d "{\"size\":$size}"
curl -sS --fail-with-body -X POST \
"$API/repositories/$CAVS_REPO_ID/uploads/$session/finalize" \
"${AUTH[@]}" -H "Idempotency-Key: $key" > /dev/null
echo "$oid" # the pin for downstream jobs
}
cavs_download() { # cavs_download <oid> <dest>
local oid="$1" dest="$2" url
url=$(curl -sS --fail-with-body -X POST \
"$API/repositories/$CAVS_REPO_ID/downloads/authorize" \
"${AUTH[@]}" "${JSON[@]}" -d "{\"oids\":[\"$oid\"]}" \
| jq -r '.objects[0].download_url')
[ "$url" != "null" ] || { echo "object $oid not found" >&2; return 1; }
curl -sS --fail-with-body -o "$dest.part" "$url"
echo "$oid $dest.part" | sha256sum -c - >/dev/null # verify before committing
mv "$dest.part" "$dest"
}
"$@"
Using it
OID=$(./scripts/cavs.sh cavs_upload ./out/model.safetensors "build-$CI_COMMIT_SHA")
./scripts/cavs.sh cavs_download "$OID" ./restored/model.safetensors
.gitlab-ci.yml
variables:
CAVS_API: https://cavsnode.com
# CAVS_TOKEN and CAVS_REPO_ID as masked/protected CI variables
stages: [build, publish, consume]
build:
stage: build
script:
- make model
artifacts:
paths: [out/model.safetensors]
expire_in: 1 hour
publish:
stage: publish
image: alpine:3.20
before_script:
- apk add --no-cache curl jq coreutils bash
script:
- OID=$(./scripts/cavs.sh cavs_upload ./out/model.safetensors "gl-$CI_PIPELINE_ID")
- echo "MODEL_OID=$OID" >> build.env
artifacts:
reports:
dotenv: build.env
consume:
stage: consume
needs: [publish]
image: alpine:3.20
before_script:
- apk add --no-cache curl jq coreutils bash
script:
- ./scripts/cavs.sh cavs_download "$MODEL_OID" ./model.safetensors

The dotenv report passes the oid to later jobs, so downstream stages consume the exact bytes this pipeline produced.

before_script:
- git lfs install --local
- git config lfs.url "$CAVS_API/api/v1/repositories/$CAVS_REPO_ID/lfs"
- git config --add http.$CAVS_API/.extraHeader "Authorization: Bearer $CAVS_TOKEN"
- git lfs pull --include="models/**"

Pin by oid, not by version. A version tag can be overwritten; an oid is the bytes. Publish the oid as a pipeline output and have downstream jobs consume that.

Use a stable idempotency key. Idempotency-Key: build-$CI_PIPELINE_ID means a retried job resumes instead of duplicating. Without it, a flaky runner creates orphan sessions that sit around until they expire.

Verify every download. sha256sum -c costs nothing and is the difference between a corrupt deploy and a failed job.

One token per pipeline. Revocation stays surgical, and the token list’s last-used column tells you what’s still live.

Fail loudly. set -euo pipefail and curl --fail-with-body. A silently empty $OID is much worse than a red build.

Handle 429. Under a matrix build you can hit the 30 req/s limit. Retry with backoff:

Terminal window
for attempt in 1 2 3 4; do
if curl -sS --fail-with-body "$@"; then break; fi
sleep $(( 2 ** attempt ))
done

The other direction: have CAVS notify your systems when a build lands. Subscribe a webhook to repository.push or release.published and trigger the downstream pipeline from it.