CI/CD recipes
Everything here needs only curl, jq and sha256sum. For GitHub Actions,
prefer the official action.
Setup, once per platform
Section titled “Setup, once per platform”| 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 |
A reusable helper
Section titled “A reusable helper”Drop this in your repository and call it from any pipeline.
#!/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"}
"$@"OID=$(./scripts/cavs.sh cavs_upload ./out/model.safetensors "build-$CI_COMMIT_SHA")./scripts/cavs.sh cavs_download "$OID" ./restored/model.safetensorsPer-platform
Section titled “Per-platform”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.safetensorsThe dotenv report passes the oid to later jobs, so downstream stages consume the
exact bytes this pipeline produced.
Git LFS on GitLab
Section titled “Git LFS on GitLab”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/**"pipeline { agent any environment { CAVS_API = 'https://cavsnode.com' CAVS_REPO_ID = credentials('cavs-repo-id') } stages { stage('Build') { steps { sh 'make model' } } stage('Publish') { steps { withCredentials([string(credentialsId: 'cavs-token', variable: 'CAVS_TOKEN')]) { script { env.MODEL_OID = sh( script: "./scripts/cavs.sh cavs_upload ./out/model.safetensors 'jenkins-${BUILD_TAG}'", returnStdout: true, ).trim() } echo "published oid ${env.MODEL_OID}" } } } } post { always { archiveArtifacts artifacts: 'out/**', allowEmptyArchive: true } }}Use withCredentials so the token is masked in the console log. Never
echo $CAVS_TOKEN.
version: 2.1
jobs: publish: docker: - image: cimg/base:stable steps: - checkout - run: name: Install tools command: sudo apt-get update && sudo apt-get install -y jq - run: name: Build command: make model - run: name: Publish to CAVS command: | OID=$(./scripts/cavs.sh cavs_upload ./out/model.safetensors "circle-$CIRCLE_WORKFLOW_ID") echo "export MODEL_OID=$OID" >> "$BASH_ENV" - persist_to_workspace: root: . paths: [out]
workflows: build-publish: jobs: - publish: context: cavs # holds CAVS_TOKEN, CAVS_API, CAVS_REPO_IDtrigger: [main]
pool: vmImage: ubuntu-latest
variables: - group: cavs # variable group with CAVS_TOKEN (secret), CAVS_API, CAVS_REPO_ID
steps: - script: make model displayName: Build
- script: | set -euo pipefail OID=$(./scripts/cavs.sh cavs_upload ./out/model.safetensors "azdo-$(Build.BuildId)") echo "##vso[task.setvariable variable=MODEL_OID]$OID" displayName: Publish to CAVS env: CAVS_TOKEN: $(CAVS_TOKEN)The whole contract is four calls to upload and one to download. If your platform
can run curl, it can talk to CAVS.
# 1. hash → 2. session → 3. authorize+PUT → 4. complete → 5. finalize./scripts/cavs.sh cavs_upload ./artifact.bin "$UNIQUE_BUILD_KEY"See Plain HTTP for the request and response shapes.
Patterns worth adopting
Section titled “Patterns worth adopting”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:
for attempt in 1 2 3 4; do if curl -sS --fail-with-body "$@"; then break; fi sleep $(( 2 ** attempt ))doneReacting to CI from CAVS
Section titled “Reacting to CI from CAVS”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.
- Plain HTTP — the full request/response reference.
- GitHub Actions — the packaged step.
- Webhooks — pipeline triggers from CAVS events.

