Set up CI/CD publishing¶
What you'll build: An automated pipeline that publishes a .deb package to Repod every time you push a version tag.
Time: ~20 minutes
Prerequisites: Repod running and accessible from your CI runner, a repository on GitHub or GitLab
Step 1 — Create an API token¶
CI/CD systems should never use your personal admin credentials. Create a dedicated token with the minimum required role (uploader).
- Go to Settings → API Tokens
- Click Create token
- Fill in:
- Name:
gitlab-ci-prod(orgithub-actions-prod) - Role:
uploader - Expiration: leave blank (or set an annual rotation date)
- Name:
- Click Generate
- Copy the token immediately — it starts with
repod_and won't be shown again
# Log in as admin first
TOKEN=$(curl -s -X POST http://REPO_HOST:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YourPassword1!"}' \
| jq -r .access_token)
# Create the API token
curl -s -X POST http://REPO_HOST:8000/auth/api-tokens \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"gitlab-ci-prod","roles":["uploader"]}' \
| jq .
Save the token value from the response — you'll need it in the next step.
One token per pipeline
Create a separate token for each CI system (GitHub, GitLab, Jenkins…). This way you can revoke one without affecting others, and you can see which system made which upload in the audit logs.
Step 2 — Store the token as a CI secret¶
- Go to your repository → Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
REPOD_TOKEN - Value:
repod_xxxxxxxxxx(your token) - Also add
REPOD_URL=https://repo.example.com(orhttp://YOUR_HOST:8000)
- Go to your project → Settings → CI/CD → Variables
- Add variable:
- Key:
REPOD_TOKEN - Value:
repod_xxxxxxxxxx - Type: Variable
- Protected: ✅ (only available on protected branches/tags)
- Masked: ✅ (hidden in logs)
- Key:
- Add
REPOD_URLthe same way
Store the token in your CI system's secret store and expose it as the environment variable REPOD_TOKEN.
Step 3 — GitHub Actions workflow¶
Create .github/workflows/publish.yml:
name: Build and publish package
on:
push:
tags:
- 'v*' # triggers on v1.0.0, v2.3.1, etc.
jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
distribution: [jammy, focal, noble] # publish to multiple distros
steps:
- uses: actions/checkout@v4
- name: Build .deb package
run: |
# Replace with your actual build command
make deb VERSION=${GITHUB_REF_NAME#v}
ls -lh *.deb
- name: Upload to Repod
env:
REPOD_TOKEN: ${{ secrets.REPOD_TOKEN }}
REPOD_URL: ${{ secrets.REPOD_URL }}
run: |
DEB_FILE=$(ls *.deb | head -1)
echo "Uploading $DEB_FILE to distribution ${{ matrix.distribution }}..."
RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST "$REPOD_URL/upload/" \
-H "Authorization: Bearer $REPOD_TOKEN" \
-F "file=@$DEB_FILE" \
-F "distribution=${{ matrix.distribution }}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)
echo "Response: $BODY"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "Upload failed with HTTP $HTTP_CODE"
exit 1
fi
STATUS=$(echo "$BODY" | jq -r .status)
echo "Package status: $STATUS"
if [ "$STATUS" = "pending_review" ]; then
echo "⚠️ Package is pending CVE review by the security team."
# Don't fail — the upload succeeded, review is expected
fi
Matrix strategy
The matrix.distribution strategy runs the upload job once per distribution in parallel. Remove distributions you don't use.
Step 4 — GitLab CI pipeline¶
Create or extend .gitlab-ci.yml:
stages:
- build
- publish
variables:
REPOD_URL: "https://repo.example.com"
DISTRIBUTIONS: "jammy focal noble"
build-deb:
stage: build
image: debian:bookworm
script:
- apt-get update -qq && apt-get install -y build-essential devscripts
- make deb VERSION=${CI_COMMIT_TAG#v}
- ls -lh *.deb
artifacts:
paths:
- "*.deb"
expire_in: 1 day
only:
- tags
publish-to-repod:
stage: publish
image: curlimages/curl:latest
needs: [build-deb]
script:
- DEB_FILE=$(ls *.deb | head -1)
- |
for DIST in $DISTRIBUTIONS; do
echo "Publishing $DEB_FILE to $DIST..."
curl -sf -X POST "$REPOD_URL/upload/" \
-H "Authorization: Bearer $REPOD_TOKEN" \
-F "file=@$DEB_FILE" \
-F "distribution=$DIST" \
|| { echo "Failed to upload to $DIST"; exit 1; }
echo "✅ Published to $DIST"
done
only:
- tags
environment:
name: production
Store REPOD_TOKEN in Settings → CI/CD → Variables (masked + protected).
Step 5 — Generic shell script¶
For Jenkins, Drone, Woodpecker, or any other system:
#!/usr/bin/env bash
# Usage: ./scripts/publish-deb.sh mypackage_1.0.0_amd64.deb jammy
set -euo pipefail
DEB_FILE="${1:?Usage: $0 <file.deb> <distribution>}"
DISTRIBUTION="${2:?Usage: $0 <file.deb> <distribution>}"
REPOD_URL="${REPOD_URL:?REPOD_URL env var not set}"
REPOD_TOKEN="${REPOD_TOKEN:?REPOD_TOKEN env var not set}"
echo "📦 Publishing $DEB_FILE → $DISTRIBUTION"
RESPONSE=$(curl -sf -X POST "$REPOD_URL/upload/" \
-H "Authorization: Bearer $REPOD_TOKEN" \
-F "file=@$DEB_FILE" \
-F "distribution=$DISTRIBUTION")
STATUS=$(echo "$RESPONSE" | jq -r .status)
echo "✅ Upload complete — status: $STATUS"
if [ "$STATUS" = "pending_review" ]; then
echo "⚠️ Security team review required before this package is published."
fi
Verify a successful upload¶
After your pipeline runs, verify the package is available:
# Check the API
curl -s -H "Authorization: Bearer $REPOD_TOKEN" \
"$REPOD_URL/packages/?q=your-package-name" | jq .
# Check the audit log (last upload entry)
curl -s -H "Authorization: Bearer $REPOD_TOKEN" \
"$REPOD_URL/artifacts/audit/logs" | jq '.[-1]'
In the web UI, go to Packages and search for your package name.
Best practices¶
| Practice | Why |
|---|---|
| One API token per CI system | Isolate blast radius if a token is leaked; separate audit trail per system |
Use uploader role only |
CI doesn't need to approve CVEs or manage users |
| Set an expiration date | Rotate tokens annually; set a calendar reminder |
Check status in the response |
pending_review means CVEs were found — alert your security team |
| Never log the token | Mask it in your CI system; use secrets, never hardcode |
| Test uploads on a staging distribution first | Use jammy-staging before promoting to jammy |
Next steps¶
- API Reference → — full upload API documentation
- Rotate API tokens → — annual token rotation procedure
- CVE review workflow → — what happens when the pipeline flags a package