Skip to content

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).

  1. Go to Settings → API Tokens
  2. Click Create token
  3. Fill in:
    • Name: gitlab-ci-prod (or github-actions-prod)
    • Role: uploader
    • Expiration: leave blank (or set an annual rotation date)
  4. Click Generate
  5. 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

  1. Go to your repository → Settings → Secrets and variables → Actions
  2. Click New repository secret
  3. Name: REPOD_TOKEN
  4. Value: repod_xxxxxxxxxx (your token)
  5. Also add REPOD_URL = https://repo.example.com (or http://YOUR_HOST:8000)
  1. Go to your project → Settings → CI/CD → Variables
  2. Add variable:
    • Key: REPOD_TOKEN
    • Value: repod_xxxxxxxxxx
    • Type: Variable
    • Protected: ✅ (only available on protected branches/tags)
    • Masked: ✅ (hidden in logs)
  3. Add REPOD_URL the 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:

.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:

.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:

scripts/publish-deb.sh
#!/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