reprepro¶
 reprepro is used as a local repository for deb packages.
Some apps, like SOPS, release deb files, but are not a part of the normal repository. Hosting them locally, allows me to download the package once and then easily update on all other containers.
 Installation¶
  
 Config¶
 
 Apache¶
 /etc/apache2/apache2.conf
/etc/apache2/conf-availabe/repos.conf
cat <<EOF > /etc/apache2/conf-availabe/repos.conf 
# /etc/apache2/conf.available/repos.conf
# Apache HTTP Server 2.4
Alias /repos/apt/debian /srv/reprepro/debian
<Directory /srv/reprepro/ >
        # We want the user to be able to browse the directory manually
        Options Indexes FollowSymLinks Multiviews
        Require all granted
</Directory>
# This syntax supports several repositories, e.g. one for Debian, one for Ubuntu.
# Replace * with debian, if you intend to support one distribution only.
<Directory "/srv/reprepro/*/db/">
        Require all denied
</Directory>
<Directory "/srv/reprepro/*/conf/">
        Require all denied
</Directory>
<Directory "/srv/reprepro/*/incoming/">
        Require all denied
</Directory>
EOF
# /etc/apache2/conf.available/repos.conf
# Apache HTTP Server 2.4
Alias /repos/apt/debian /srv/reprepro/debian
<Directory /srv/reprepro/ >
        # We want the user to be able to browse the directory manually
        Options Indexes FollowSymLinks Multiviews
        Require all granted
</Directory>
# This syntax supports several repositories, e.g. one for Debian, one for Ubuntu.
# Replace * with debian, if you intend to support one distribution only.
<Directory "/srv/reprepro/*/db/">
        Require all denied
</Directory>
<Directory "/srv/reprepro/*/conf/">
        Require all denied
</Directory>
<Directory "/srv/reprepro/*/incoming/">
        Require all denied
</Directory>
 Config¶
 
 Repository¶
 Make directories
shell ( [ -d /srv/reprepro/debian/conf ] || mkdir -p /srv/reprepro/debian/conf [ -d /srv/reprepro/ubuntu/conf ] || mkdir -p /srv/reprepro/ubuntu/conf )
Generate new gpg keys
gpg --list-keys  
 pub  2048R/489CD644 2014-07-15  
 uid         Your Name <[email protected]>  
 sub  2048R/870B8E2D 2014-07-15
Get short fingerprint
gpg -k [email protected] | sed -n '2p'| sed 's/ //g' | tail -c 9
Export public gpg key
/srv/reprepo/<dist>/conf/distributions
(
  cat <<EOF > /srv/reprepo/debian/conf/distributions
  Origin: Debian  
  Label: Bookworm apt repository  
  Codename: bookworm
  Architectures: i386 amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Debian stable - Bookworm  
  DebOverride: override.bookworm
  DscOverride: override.bookworm
  SignWith: 089C9FAF 
  Origin: Debian  
  Label: Bullseye apt repository
  Codename: bullseye
  Architectures: i386 amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Debian stable - Bullseye  
  DebOverride: override.bullseye
  DscOverride: override.bullseye
  SignWith: 089C9FAF
  Origin: Debian
  Label: Trixie apt repository
  Codename: trixie
  Architectures: i386 amd64 arm64 armhf
  Components: main
  Description: Apt repository for Debian stable - Trixie
  DebOverride: override.trixie
  DscOverride: override.trixie
  SignWith: 089C9FAF
  EOF
  cat <<EOF > /srv/reprepo/ubuntu/conf/distributions
  Origin: Ubuntu
  Label: Questing apt repository
  Codename: questing
  Architectures: amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Ubuntu stable - Questing
  DebOverride: override.questing
  DscOverride: override.questing
  SignWith: 089C9FAF 
  Origin: Ubuntu
  Label: Plucky apt repository
  Codename: plucky
  Architectures: amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Ubuntu stable - Plucky
  DebOverride: override.plucky
  DscOverride: override.plucky
  SignWith: 089C9FAF 
  Origin: Ubuntu
  Label: Oracular apt repository
  Codename: oracular
  Architectures: amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Ubuntu stable - Oracular
  DebOverride: override.oracular
  DscOverride: override.oracular
  SignWith: 089C9FAF 
  Origin: Ubuntu
  Label: Noble apt repository
  Codename: noble
  Architectures: amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Ubuntu stable - Noble
  DebOverride: override.noble
  DscOverride: override.noble
  SignWith: 089C9FAF 
  Origin: Ubuntu
  Label: Jammy apt repository
  Codename: jammy
  Architectures: amd64 arm64 armhf
  Components: main  
  Description: Apt repository for Ubuntu stable - Jammy
  DebOverride: override.jammy
  DscOverride: override.jammy
  SignWith: 089C9FAF 
  EOF
)
Origin: Debian  
Label: Bookworm apt repository  
Codename: bookworm
Architectures: i386 amd64 arm64 armhf
Components: main  
Description: Apt repository for Debian stable - Bookworm  
DebOverride: override.bookworm
DscOverride: override.bookworm
SignWith: 089C9FAF 
Origin: Debian  
Label: Bullseye apt repository
Codename: bullseye
Architectures: i386 amd64 arm64 armhf
Components: main  
Description: Apt repository for Debian stable - Bullseye  
DebOverride: override.bullseye
DscOverride: override.bullseye
SignWith: 089C9FAF
Origin: Debian
Label: Trixie apt repository
Codename: trixie
Architectures: i386 amd64 arm64 armhf
Components: main
Description: Apt repository for Debian stable - Trixie
DebOverride: override.trixie
DscOverride: override.trixie
SignWith: 089C9FAF
Origin: Ubuntu
Label: Questing apt repository
Codename: questing
Architectures: amd64 arm64 armhf
Components: main  
Description: Apt repository for Ubuntu stable - Questing
DebOverride: override.questing
DscOverride: override.questing
SignWith: 089C9FAF 
Origin: Ubuntu
Label: Plucky apt repository
Codename: plucky
Architectures: amd64 arm64 armhf
Components: main  
Description: Apt repository for Ubuntu stable - Plucky
DebOverride: override.plucky
DscOverride: override.plucky
SignWith: 089C9FAF 
Origin: Ubuntu
Label: Oracular apt repository
Codename: oracular
Architectures: amd64 arm64 armhf
Components: main  
Description: Apt repository for Ubuntu stable - Oracular
DebOverride: override.oracular
DscOverride: override.oracular
SignWith: 089C9FAF 
Origin: Ubuntu
Label: Noble apt repository
Codename: noble
Architectures: amd64 arm64 armhf
Components: main  
Description: Apt repository for Ubuntu stable - Noble
DebOverride: override.noble
DscOverride: override.noble
SignWith: 089C9FAF 
Origin: Ubuntu
Label: Jammy apt repository
Codename: jammy
Architectures: amd64 arm64 armhf
Components: main  
Description: Apt repository for Ubuntu stable - Jammy
DebOverride: override.jammy
DscOverride: override.jammy
SignWith: 089C9FAF 
/srv/reprepo/<dist>/conf/options
/srv/reprepo/<dist>/conf/override.<codename>
(
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/debian/conf/override.bullseye -O /srv/reprepro/debian/conf/override.bullseye
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/debian/conf/override.bookworm -O /srv/reprepro/debian/conf/override.bookworm
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/debian/conf/override.trixie -O /srv/reprepro/debian/conf/override.trixie
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/ubuntu/conf/override.questing -O /srv/reprepro/ubuntu/conf/override.questing
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/ubuntu/conf/override.plucky -O /srv/reprepro/ubuntu/conf/override.plucky
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/ubuntu/conf/override.oracular -O /srv/reprepro/ubuntu/conf/override.oracular
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/ubuntu/conf/override.noble -O /srv/reprepro/ubuntu/conf/override.noble
  sudo wget https://github.com/nicholaswilde/homelab/raw/refs/heads/main/pve/reprepro/ubuntu/conf/override.jammy -O /srv/reprepro/ubuntu/conf/override.jammy
)
(
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/debian/conf/override.bullseye /srv/reprepro/debian/conf/override.bullseye
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/debian/conf/override.bookworm /srv/reprepro/debian/conf/override.bookworm
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/debian/conf/override.trixie /srv/reprepro/debian/conf/override.trixie
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/ubuntu/conf/override.questing /srv/reprepro/ubuntu/conf/override.questing
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/ubuntu/conf/override.plucky /srv/reprepro/ubuntu/conf/override.plucky
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/ubuntu/conf/override.oracular /srv/reprepro/ubuntu/conf/override.oracular
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/ubuntu/conf/override.noble /srv/reprepro/ubuntu/conf/override.noble
  sudo ln -fs /root/git/nicholaswilde/homelab/pve/reprepro/ubuntu/conf/override.jammy /srv/reprepro/ubuntu/conf/override.jammy
)
(
  sudo touch /srv/reprepro/debian/conf/override.bookworm
  sudo touch /srv/reprepro/debian/conf/override.bullseye
  sudo touch /srv/reprepro/debian/conf/override.trixie
  sudo touch /srv/reprepro/ubuntu/conf/override.questing
  sudo touch /srv/reprepro/ubuntu/conf/override.plucky
  sudo touch /srv/reprepro/ubuntu/conf/override.oracular
  sudo touch /srv/reprepro/ubuntu/conf/override.noble
  sudo touch /srv/reprepro/ubuntu/conf/override.jammy
)
 Environmental File¶
 A .env file is used to set variables that are used with task and scripts.
Edit the .env file with your preferred text editor.
.env
# Used by multiple scripts
GITHUB_TOKEN=
BASE_DIR=/srv/reprepro
ENABLE_NOTIFICATIONS="true"
DEBUG="false"
# sync-check.sh
SYNC_APPS_GITHUB_REPOS=("getsops/sops" "go-task/task" "sharkdp/fd" "sharkdp/bat" "localsend/localsend" "BurntSushi/ripgrep" "muesli/duf" "charmbracelet/glow")
# package-neovim.sh
PACKAGE_APPS=("BurntSushi/ripgrep:file_strip:rg:.*-\K[^-]+(?=-unknown-linux-gnu)" "eza-community/eza:all::(?<=_)[^-]*" "chmln/sd:file_strip::.*-\K[^-]+(?=-unknown-linux)" "aristocratos/btop:file_path:btop/bin/btop:(?<=btop-)[^-]+(?=-linux-)")
# Mailrise notifications
MAILRISE_URL='smtp://smtp.l.nicholaswilde.io:8025'
MAILRISE_FROM='[email protected]'
MAILRISE_RCPT='[email protected]'
# upload-neovim.sh
REMOTE_IP=192.168.2.32
REMOTE_USER=root
REMOTE_PATH=/root/
 Adding a New Release Codename¶
 To add a new release codename to reprepro:
- Add a new 
override.<codename>file in/srv/reprepro/<dist>/conf. - Add a new entry to 
/srv/reprepro/<dist>/conf/distributionsfile. 
/srv/reprepro/<dist>/conf/distributions
 Usage¶
 
 Server¶
 Add deb file to reprepro.
(
  sudo reprepro -b /srv/reprepro/debian -C main includedeb bookworm sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/debian -C main includedeb bullseye sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/debian -C main includedeb trixie sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/ubuntu -C main includedeb questing sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/ubuntu -C main includedeb plucky sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/ubuntu -C main includedeb oracular sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/ubuntu -C main includedeb noble sops_3.9.4_amd64.deb
  sudo reprepro -b /srv/reprepro/ubuntu -C main includedeb jammy sops_3.9.4_amd64.deb
)
 Client¶
 Download gpg key
Add repo and install.
/etc/apt/sources.list.d/reprepro.list
 Copy all package from one codename to another from noble to questing.¶
  
 List all package information¶
  
 Regenerate the Repository Index¶
 This can fix BADSIG errors shown on remote hosts.
The badsig error means the Release.gpg file for that codename does not contain a valid signature for the Release file. The Release file itself contains a list of all other index files (like Packages.gz) and their checksums.
 Scripts¶
 Some scripts are provided to help with common tasks.
 Update Reprepro¶
 The script update-reprepro.sh is used to compare the latest released versions of the apps specified with the SYNC_APPS_GITHUB_REPOS and PACKAGE_APPS variables in the .env file to the local versions.
If out of date, the compressed archives specified in the PACKAGE_APPS variable are downloaded, packaged into deb files, and added to reprepro and deb files located in the SYNC_APPS_GITHUB_REPOS variable are downloaded and add to reprepro.
package-apps.sh
#!/usr/bin/env bash
################################################################################
#
# Script Name: update-reprepro.sh
# ----------------
# Downloads application tar.gz and .deb files, packages them as needed, and
# adds them to a reprepro repository.
#
# Combines the functionality of package-apps.sh and sync-check.sh.
#
# @author Nicholas Wilde, 0xb299a622
# @date 18 Oct 2025
# @version 1.0.0
#
################################################################################
# Options
# set -e
# set -o pipefail
# These are constants
readonly BLUE=$(tput setaf 4)
readonly RED=$(tput setaf 1)
readonly YELLOW=$(tput setaf 3)
readonly PURPLE=$(tput setaf 5)
readonly RESET=$(tput sgr0)
readonly SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
readonly DEBIAN_CODENAMES=($(grep -oP '(?<=Codename: ).*' "${SCRIPT_DIR}/debian/conf/distributions"))
readonly UBUNTU_CODENAMES=($(grep -oP '(?<=Codename: ).*' "${SCRIPT_DIR}/ubuntu/conf/distributions"))
# Default variables
BASE_DIR="/srv/reprepro"
ENABLE_NOTIFICATIONS="false"
DEBUG="false"
if [ ! -f "${SCRIPT_DIR}/.env" ]; then
  echo "ERRO[$(date +'%Y-%m-%d %H:%M:%S')] The .env file is missing. Please create it." >&2
  exit 1
fi
source "${SCRIPT_DIR}/.env"
APPS_OUT_OF_DATE="false"
UPDATE_SUCCESS="true"
SUCCESSFUL_APPS=()
FAILED_APPS=()
# Logging function
function log() {
  local type="$1"
  local color="$RESET"
  if [ "${type}" = "DEBU" ] && [ "${DEBUG}" != "true" ]; then
    return 0
  fi
  case "$type" in
    INFO)
      color="$BLUE";;
    WARN)
      color="$YELLOW";;
    ERRO)
      color="$RED";;
    DEBU)
      color="$PURPLE";;
    *)
      type="LOGS";;
  esac
  if [[ -t 0 ]]; then
    local message="$2"
    echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${message}"
  else
    while IFS= read -r line; do
      echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${line}"
    done
  fi
}
function usage() {
  cat <<EOF
Usage: $0 [options]
Manages Debian packages in the reprepro repository.
This script can:
1. Download application source (tar.gz), package it into a .deb, and add it.
2. Download pre-compiled .deb packages and add them.
Options:
  -d, --debug         Enable debug mode, which prints more info.
  -r, --remove <pkg>  Remove a package from the repository.
  -h, --help          Display this help message.
EOF
}
# Cleanup function to remove temporary directory
function cleanup() {
  if [ -d "${TEMP_PATH}" ]; then
    log "INFO" "Cleaning up temporary directory: ${TEMP_PATH}"
    rm -rf "${TEMP_PATH}"
  fi
}
# Checks if a command exists.
function command_exists() {
  command -v "$1" >/dev/null 2>&1
}
function check_dependencies() {
  if ! command_exists reprepro; then
    log "ERRO" "reprepro is not installed."
    exit 1
  fi
  if ! command_exists curl || ! command_exists jq || ! command_exists dpkg-deb; then
    log "ERRO" "Required dependencies (curl, jq, dpkg-deb) are not installed."
    exit 1
  fi
}
function check_root(){
  if [ "$UID" -ne 0 ]; then
    log "ERRO" "Please run as root or with sudo."
    exit 1
  fi
}
function make_temp_dir(){
  export TEMP_PATH=$(mktemp -d)
  if [ ! -d "${TEMP_PATH}" ]; then
    log "ERRO" "Could not create temp dir"
    exit 1
  fi
  log "INFO" "Temp path: ${TEMP_PATH}"
}
function get_latest_version() {
  local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
  local curl_args=('-s')
  if [ -n "${GITHUB_TOKEN}" ]; then
    curl_args+=('-H' "Authorization: Bearer ${GITHUB_TOKEN}")
  fi
  export json_response=$(curl "${curl_args[@]}" "${api_url}")
  if ! echo "${json_response}" | jq -e '.tag_name' >/dev/null 2>&1; then
    log "ERRO" "Failed to get latest version for ${APP_NAME} from GitHub API."
    echo "${json_response}"
    return 1
  fi
  export TAG_NAME=$(echo "${json_response}" | jq -r '.tag_name')
  export LATEST_VERSION=${TAG_NAME#v}
  export PUBLISHED_AT=$(echo "${json_response}" | jq -r '.published_at')
  export SOURCE_DATE_EPOCH=$(date -d "${PUBLISHED_AT}" +%s)
  log "INFO" "Latest ${APP_NAME} version: ${LATEST_VERSION} (tag: ${TAG_NAME})"
}
function get_current_version(){
  CURRENT_VERSION=$(sudo reprepro --confdir "${BASE_DIR}/ubuntu/conf/" list "${UBUNTU_CODENAMES[0]}" "${APP_NAME}" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/[-+].*//' || true)
  export CURRENT_VERSION
  log "INFO" "Current ${APP_NAME} version in reprepro: ${CURRENT_VERSION}"
}
function remove_package() {
  local package_name=$1
  log "INFO" "Forcefully removing existing '${package_name}' packages from reprepro to ensure a clean state..."
  for codename in "${UBUNTU_CODENAMES[@]}"; do
    log "INFO" "Attempting to remove '${package_name}' from Ubuntu ${codename}"
    reprepro -b "${BASE_DIR}/ubuntu" remove "${codename}" "${package_name}"  2>&1 | log "DEBU" || true
  done
  for codename in "${DEBIAN_CODENAMES[@]}"; do
    log "INFO" "Attempting to remove '${package_name}' from Debian ${codename}"
    reprepro -b "${BASE_DIR}/debian" remove "${codename}" "${package_name}"  2>&1 | log "DEBU" || true
  done
  log "INFO" "Searching for and removing old '${package_name}' .deb files from the pool..."
  find "${BASE_DIR}/debian/pool/" -name "${package_name}_*.deb" -delete
  find "${BASE_DIR}/ubuntu/pool/" -name "${package_name}_*.deb" -delete
  log "INFO" "Pool cleanup complete."
}
function extract_binary() {
  local extract_type="$1"
  local tarball_path="$2"
  local package_dir="$3"
  local folder_name="$4"
  local bin_name="$5"
  local arch_github="$7"
  log "INFO" "Extracting ${APP_NAME} binary using strategy: ${extract_type}"
  local extract_dest="${package_dir}/usr/local/bin/"
  case "${extract_type}" in
    "all")
      tar -xf "${tarball_path}" -C "${extract_dest}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to extract ${tarball_path}"; return 1; }
      ;;
    "file_strip")
      tar -xf "${tarball_path}" -C "${extract_dest}" --strip-components=1 "${folder_name}/${bin_name}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to extract ${tarball_path}"; return 1; }
      ;;
    "file")
      tar -xf "${tarball_path}" -C "${extract_dest}" "${bin_name}"  2>&1 | log "DEBU" || { log "ERRO" "Failed to extract ${tarball_path}"; return 1; }
      ;;
    "file_path")
      local full_bin_path="${bin_name//\$\{ARCH\}/${arch_github}}"
      local dir_path
      dir_path=$(dirname "${full_bin_path}")
      local strip_components=0
      if [ "${dir_path}" != "." ]; then
        strip_components=$(echo "${dir_path}" | awk -F'/' '{print NF}')
      fi
      tar -xf "${tarball_path}" -C "${extract_dest}" --strip-components="${strip_components}" "${full_bin_path}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to extract ${full_bin_path} from ${tarball_path}"; return 1; }
      local extracted_bin_name
      extracted_bin_name=$(basename "${full_bin_path}")
      if [ "${extracted_bin_name}" != "${APP_NAME}" ]; then
        log "INFO" "Renaming extracted binary from ${extracted_bin_name} to ${APP_NAME}"
        mv "${extract_dest}/${extracted_bin_name}" "${extract_dest}/${APP_NAME}" || { log "ERRO" "Failed to rename binary."; return 1; }
      fi
      ;;
    *)
      log "ERRO" "Unknown extraction type: ${extract_type}"
      return 1;;
  esac
  return 0
}
function package_and_add() {
  local arch_github=$1
  local arch_debian=$2
  local tarball_name=$3
  local extract_type=$4
  local bin_name=$5
  local folder_name="${tarball_name%.tar.gz}"
  folder_name="${folder_name%.tar.bz2}"
  folder_name="${folder_name%.tbz}"
  log "INFO" "Processing architecture: ${arch_github} as ${arch_debian}"
  local download_url
  download_url=$(echo "${json_response}" | jq -r --arg pkg_name "$tarball_name" '.assets[] | select(.name==$pkg_name) | .browser_download_url')
  if [ -z "${download_url}" ]; then
    log "ERRO" "Failed to get download url for ${tarball_name}"
    return 1
  fi
  local tarball_path="${TEMP_PATH}/${tarball_name}"
  log "INFO" "Downloading ${tarball_name}..."
  wget -q "${download_url}" -O "${tarball_path}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to download ${tarball_name}"; return 1; }
  local package_dir="${TEMP_PATH}/${APP_NAME}_${LATEST_VERSION}_${arch_debian}"
  mkdir -p "${package_dir}/usr/local/bin"
  mkdir -p "${package_dir}/DEBIAN"
  extract_binary "${extract_type}" "${tarball_path}" "${package_dir}" "${folder_name}" "${bin_name}" "${arch_github}" || return 1
  log "INFO" "Creating control file..."
  cat << EOF > "${package_dir}/DEBIAN/control"
Package: ${APP_NAME}
Version: ${LATEST_VERSION}
Section: utils
Priority: optional
Architecture: ${arch_debian}
Maintainer: Nicholas Wilde <[email protected]>
Description: ${DESCRIPTION}
EOF
  log "INFO" "Building .deb package..."
  local deb_file="${APP_NAME}_${LATEST_VERSION}_${arch_debian}.deb"
  # dpkg-deb --build "${package_dir}" "${TEMP_PATH}/${deb_file}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to build .deb package for ${APP_NAME} ${LATEST_VERSION} ${arch_debian}"; return 1; }
  local build_output=$(dpkg-deb --build "${package_dir}" "${TEMP_PATH}/${deb_file}" 2>&1)
  local exit_status=$?
  echo "${build_output}" | log "DEBU"
  if [[ ${exit_status} -ne 0 ]]; then
    log "ERRO" "Failed to build .deb package for ${APP_NAME} ${LATEST_VERSION} ${arch_debian}"
    return 1
  fi
  log "INFO" "Adding ${deb_file} to reprepro..."
  for codename in "${UBUNTU_CODENAMES[@]}"; do
    reprepro -b "${BASE_DIR}/ubuntu" -C main includedeb "${codename}" "${TEMP_PATH}/${deb_file}" 2>&1 | log "DEBU" || true
  done
  for codename in "${DEBIAN_CODENAMES[@]}"; do
    reprepro -b "${BASE_DIR}/debian" -C main includedeb "${codename}" "${TEMP_PATH}/${deb_file}"  2>&1 | log "DEBU" || true
  done
}
function download_and_add_deb() {
  local package_name=$1
  log "INFO" "Processing package: ${package_name}"
  local download_url
  download_url=$(echo "${json_response}" | jq -r --arg pkg_name "$package_name" '.assets[] | select(.name==$pkg_name) | .browser_download_url')
  if [ -z "${download_url}" ]; then
    log "ERRO" "Failed to get download url for ${package_name}"
    return 1
  fi
  local package_path="${TEMP_PATH}/${package_name}"
  log "INFO" "Downloading ${package_name}..."
  wget -q "${download_url}" -O "${package_path}" || { log "ERRO" "Failed to download ${package_name}"; return 1; }
  log "INFO" "Adding ${package_name} to reprepro..."
  for codename in "${UBUNTU_CODENAMES[@]}"; do
    reprepro -b "${BASE_DIR}/ubuntu" -C main includedeb "${codename}" "${package_path}" 2>&1 | log "DEBU" || true
  done
  for codename in "${DEBIAN_CODENAMES[@]}"; do
    reprepro -b "${BASE_DIR}/debian" -C main includedeb "${codename}" "${package_path}" 2>&1 | log "DEBU" || true
  done
}
function update_app_from_source() {
  local app_name="$1"
  local github_repo="$2"
  local extract_type="$3"
  local bin_name="$4"
  local arch_regexp="$5"
  export APP_NAME="${app_name}"
  export GITHUB_REPO="${github_repo}"
  print_separator "Processing source package: ${APP_NAME}"
  get_latest_version || { FAILED_APPS+=("${app_name}"); return 1; }
  get_current_version
  if [[ "${LATEST_VERSION}" == "${CURRENT_VERSION}" ]]; then
    log "INFO" "${APP_NAME} is already up-to-date: ${CURRENT_VERSION}"
    return 0
  fi
  APPS_OUT_OF_DATE="true"
  log "INFO" "New version available for ${APP_NAME}: ${LATEST_VERSION}"
  export DESCRIPTION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}" | jq -r '.description' | sed -e 's/:\w\+://g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
  local linux_tarballs
  linux_tarballs=$(echo "${json_response}" | jq -r '.assets[] | select(.name | (endswith(".tar.gz") or endswith(".tar.bz2") or endswith(".tbz")) and (contains("openbsd") | not) and (contains("darwin") | not) and (contains("freebsd")| not) and (contains("android") | not) and (contains("windows") | not)) | .name')
  local app_update_failed="false"
  for tarball in ${linux_tarballs}; do
    local github_arch
    github_arch=$(echo "${tarball}" | grep -oP "${arch_regexp}")
    local debian_arch=""
    case "${github_arch}" in
      "amd64"|"x86_64") debian_arch="amd64";;
      "arm64"|"aarch64") debian_arch="arm64";;
      "armv7"|"armhf"|"arm") debian_arch="armhf";;
      "386") debian_arch="i386";;
      *)
        log "WARN" "Unsupported architecture for ${APP_NAME}: ${github_arch//$'\n'/ }. Skipping."
        continue;;
    esac
    package_and_add "${github_arch}" "${debian_arch}" "${tarball}" "${extract_type}" "${bin_name}" || { app_update_failed="true"; continue; }
  done
  get_current_version
  if [[ "${LATEST_VERSION}" != "${CURRENT_VERSION}" || "${app_update_failed}" == "true" ]]; then
    log "ERRO" "Failed to update ${APP_NAME} to ${LATEST_VERSION}."
    FAILED_APPS+=("${app_name}: ${LATEST_VERSION}")
    return 1
  else
    log "INFO" "Successfully updated ${APP_NAME} to ${LATEST_VERSION}."
    SUCCESSFUL_APPS+=("${app_name}: ${LATEST_VERSION}")
    return 0
  fi
}
function update_app_from_deb() {
  local app_config="$1"
  local github_repo
  local app_name
  if [[ "${app_config}" == *"|"* ]]; then
    IFS='|' read -r github_repo app_name <<< "${app_config}"
  else
    github_repo="${app_config}"
    app_name=$(basename "${github_repo}")
  fi
  export GITHUB_REPO="${github_repo}"
  export APP_NAME="${app_name}"
  print_separator "Processing deb package: ${APP_NAME} from ${GITHUB_REPO}"
  get_latest_version || { FAILED_APPS+=("${APP_NAME}"); return 1; }
  get_current_version
  if [[ "${LATEST_VERSION}" == "${CURRENT_VERSION}" ]]; then
    log "INFO" "${APP_NAME} is already up-to-date: ${CURRENT_VERSION}"
    return 0
  fi
  APPS_OUT_OF_DATE="true"
  log "INFO" "New version available for ${APP_NAME}: ${LATEST_VERSION}"
  local linux_debs
  linux_debs=$(echo "${json_response}" | jq -r '.assets[] | select(.name | endswith(".deb") and (contains("musl") | not)) | .name')
  local app_update_failed="false"
  for deb in ${linux_debs}; do
    download_and_add_deb "${deb}" || { app_update_failed="true"; continue; }
  done
  get_current_version
  if [[ "${LATEST_VERSION}" != "${CURRENT_VERSION}" || "${app_update_failed}" == "true" ]]; then
    log "ERRO" "Failed to update ${APP_NAME} to ${LATEST_VERSION}."
    FAILED_APPS+=("${APP_NAME}: ${LATEST_VERSION}")
    return 1
  else
    log "INFO" "Successfully updated ${APP_NAME} to ${LATEST_VERSION}."
    SUCCESSFUL_APPS+=("${APP_NAME}: ${LATEST_VERSION}")
    return 0
  fi
}
function print_separator(){
  local msg=$1
  local header=$(printf '%.0s-' {1..60})
  log "INFO" "${header}"
  log "INFO" "${msg}"
  log "INFO" "${header}"
}
function update_tea() {
  export GITHUB_REPO="gitea/tea"
  local binary_name="tea"
  export APP_NAME="gitea-tea" # This is the package name
  # local header=$(printf '%.0s-' {1..60})
  # log "INFO" "--------------------------------------------------"
  # log "INFO" "${header}"
  # log "INFO" "Processing binary package: ${binary_name} from gitea.com as ${APP_NAME}"
  print_separator "Processing binary package: ${binary_name} from gitea.com as ${APP_NAME}"
  # log "INFO" "--------------------------------------------------"
  # log "INFO" "${header}"
  local api_url="https://gitea.com/api/v1/repos/${GITHUB_REPO}/releases/latest"
  export json_response=$(curl -s "${api_url}")
  if ! echo "${json_response}" | jq -e '.tag_name' >/dev/null 2>&1; then
    log "ERRO" "Failed to get latest version for ${APP_NAME} from Gitea API."
    echo "${json_response}"
    FAILED_APPS+=("${APP_NAME}")
    return 1
  fi
  export TAG_NAME=$(echo "${json_response}" | jq -r '.tag_name')
  export LATEST_VERSION=${TAG_NAME#v}
  export PUBLISHED_AT=$(echo "${json_response}" | jq -r '.published_at')
  export SOURCE_DATE_EPOCH=$(date -d "${PUBLISHED_AT}" +%s)
  log "INFO" "Latest ${APP_NAME} version: ${LATEST_VERSION} (tag: ${TAG_NAME})"
  get_current_version
  if [[ "${LATEST_VERSION}" == "${CURRENT_VERSION}" ]]; then
    log "INFO" "${APP_NAME} is already up-to-date: ${CURRENT_VERSION}"
    return 0
  fi
  APPS_OUT_OF_DATE="true"
  log "INFO" "New version available for ${APP_NAME}: ${LATEST_VERSION}"
  export DESCRIPTION=$(curl -s "https://gitea.com/api/v1/repos/${GITHUB_REPO}" | jq -r '.description' | sed -e 's/:\w\+://g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
  local linux_binaries
  linux_binaries=$(echo "${json_response}" | jq -r '.assets[] | select(.name | contains("-linux-") and (endswith(".asc") | not) and (endswith(".sha256") | not) and (endswith(".tar.gz") | not) and (endswith(".zip") | not)) | .name')
  local app_update_failed="false"
  for binary in ${linux_binaries}; do
    local gitea_arch
    gitea_arch=$(echo "${binary}" | sed -n "s/${binary_name}-${LATEST_VERSION}-linux-//p")
    local debian_arch=""
    case "${gitea_arch}" in
      "amd64") debian_arch="amd64";;
      "arm64") debian_arch="arm64";;
      "arm-7") debian_arch="armhf";;
      "386") debian_arch="i386";;
      *)
        log "WARN" "Unsupported architecture for ${APP_NAME}: ${gitea_arch}. Skipping."
        continue;;
    esac
    local download_url
    download_url=$(echo "${json_response}" | jq -r --arg pkg_name "$binary" '.assets[] | select(.name==$pkg_name) | .browser_download_url')
    if [ -z "${download_url}" ]; then
      log "ERRO" "Failed to get download url for ${binary}"
      app_update_failed="true"
      continue
    fi
    local binary_path="${TEMP_PATH}/${binary}"
    log "INFO" "Downloading ${binary}..."
    wget -q "${download_url}" -O "${binary_path}" 2>&1 | log "DEBU" || { log "ERRO" "Failed to download ${binary}"; app_update_failed="true"; continue; }
    local package_dir="${TEMP_PATH}/${APP_NAME}_${LATEST_VERSION}_${debian_arch}"
    mkdir -p "${package_dir}/usr/local/bin"
    mkdir -p "${package_dir}/DEBIAN"
    mv "${binary_path}" "${package_dir}/usr/local/bin/${binary_name}"
    chmod +x "${package_dir}/usr/local/bin/${binary_name}"
    log "INFO" "Creating control file for ${debian_arch}..."
    cat << EOF > "${package_dir}/DEBIAN/control"
Package: ${APP_NAME}
Version: ${LATEST_VERSION}
Section: utils
Priority: optional
Architecture: ${debian_arch}
Maintainer: Nicholas Wilde <[email protected]>
Description: ${DESCRIPTION}
EOF
    log "INFO" "Building .deb package for ${debian_arch}..."
    local deb_file="${APP_NAME}_${LATEST_VERSION}_${debian_arch}.deb"
    local build_output=$(dpkg-deb --build "${package_dir}" "${TEMP_PATH}/${deb_file}" 2>&1)
    local exit_status=$?
    echo "${build_output}" | log "DEBU"
    if [[ ${exit_status} -ne 0 ]]; then
      log "ERRO" "Failed to build .deb package for ${APP_NAME} ${LATEST_VERSION} ${debian_arch}"
      app_update_failed="true"
      continue
    fi
    log "INFO" "Adding ${deb_file} to reprepro..."
    for codename in "${UBUNTU_CODENAMES[@]}"; do
      sudo reprepro -b "${BASE_DIR}/ubuntu" -C main includedeb "${codename}" "${TEMP_PATH}/${deb_file}" 2>&1 | log "DEBU" || true
    done
    for codename in "${DEBIAN_CODENAMES[@]}"; do
      sudo reprepro -b "${BASE_DIR}/debian" -C main includedeb "${codename}" "${TEMP_PATH}/${deb_file}"  2>&1 | log "DEBU" || true
    done
  done
  get_current_version
  if [[ "${LATEST_VERSION}" != "${CURRENT_VERSION}" || "${app_update_failed}" == "true" ]]; then
    log "ERRO" "Failed to update ${APP_NAME} to ${LATEST_VERSION}."
    FAILED_APPS+=("${APP_NAME}: ${LATEST_VERSION}")
    return 1
  else
    log "INFO" "Successfully updated ${APP_NAME} to ${LATEST_VERSION}."
    SUCCESSFUL_APPS+=("${APP_NAME}: ${LATEST_VERSION}")
    return 0
  fi
}
function send_notification(){
  if [[ "${ENABLE_NOTIFICATIONS}" == "false" ]]; then
    log "WARN" "Notifications are disabled. Skipping."
    return 0
  fi
  if [[ -z "${MAILRISE_URL}" || -z "${MAILRISE_FROM}" || -z "${MAILRISE_RCPT}" ]]; then
    log "WARN" "Notification variables not set. Skipping notification."
    return 1
  fi
  if [[ "${APPS_OUT_OF_DATE}" == "false" ]]; then
    log "INFO" "No applications were out of date. No email notification sent."
    return 0
  fi
  local EMAIL_SUBJECT="Homelab - Update Reprepro Summary"
  local EMAIL_BODY=""
  if [[ "${UPDATE_SUCCESS}" == "true" ]]; then
    EMAIL_BODY="All out-of-date applications were successfully updated."
  else
    EMAIL_BODY="Some out-of-date applications failed to update"
  fi
  if [ ${#SUCCESSFUL_APPS[@]} -gt 0 ]; then
    EMAIL_BODY+=$'\n\nSuccessfully updated:\n'
    for app in "${SUCCESSFUL_APPS[@]}"; do
      EMAIL_BODY+="- ${app}"$'\n'
    done
  fi
  if [ ${#FAILED_APPS[@]} -gt 0 ]; then
    EMAIL_BODY+=$'\n\nFailed to update:\n'
    for app in "${FAILED_APPS[@]}"; do
      EMAIL_BODY+="- ${app}"$'\n'
    done
  fi
  log "INFO" "Sending email notification..."
  curl -s \
    --url "${MAILRISE_URL}" \
    --mail-from "${MAILRISE_FROM}" \
    --mail-rcpt "${MAILRISE_RCPT}" \
    --upload-file - <<EOF
From: Reprepro <${MAILRISE_FROM}>
To: Nicholas Wilde <${MAILRISE_RCPT}>
Subject: ${EMAIL_SUBJECT}
${EMAIL_BODY}
EOF
  log "INFO" "Email notification sent."
}
# Main function to orchestrate the script execution
function main() {
  trap cleanup EXIT
  local package_to_remove=""
  while [[ "$#" -gt 0 ]]; do
    case $1 in
      -d|--debug) DEBUG="true"; shift;;
      -r|--remove)
        if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
          package_to_remove="$2"; shift 2;
        else
          log "ERRO" "Error: Argument for $1 is missing"; usage; exit 1;
        fi;;
      -h|--help) usage; exit 0;;
      *) log "ERRO" "Unknown parameter passed: $1"; usage; exit 1;;
    esac
  done
  log "INFO" "Starting reprepro update script..."
  check_root
  if [ -n "${package_to_remove}" ]; then
    remove_package "${package_to_remove}"
    log "INFO" "Script finished."
    exit 0
  fi
  check_dependencies
  make_temp_dir
  # Process apps to be packaged from source
  if [ -z "${PACKAGE_APPS-}" ]; then
    log "WARN" "PACKAGE_APPS is not defined in .env. Skipping source packaging."
  else
    for app_config in "${PACKAGE_APPS[@]}"; do
      IFS=':' read -r github_repo extract_type bin_name arch_regexp <<< "${app_config}"
      local app_name
      app_name=$(basename "${github_repo}")
      if [ -z "${extract_type}" ]; then extract_type="file"; fi
      if [ -z "${bin_name}" ]; then bin_name="${app_name}"; fi
      if [ -z "${arch_regexp}" ]; then arch_regexp='(?<=_)[^_]+(?=\.tar\.gz)'; fi
      update_app_from_source "${app_name}" "${github_repo}" "${extract_type}" "${bin_name}" "${arch_regexp}" || UPDATE_SUCCESS="false"
    done
  fi
  # Process pre-compiled deb apps
  if [ -z "${SYNC_APPS_GITHUB_REPOS-}" ]; then
    log "WARN" "SYNC_APPS_GITHUB_REPOS is not defined in .env. Skipping deb sync."
  else
    for app_config in "${SYNC_APPS_GITHUB_REPOS[@]}"; do
      update_app_from_deb "${app_config}" || UPDATE_SUCCESS="false"
    done
  fi
  update_tea || UPDATE_SUCCESS="false"
  send_notification
  log "INFO" "Script finished."
}
# Call main to start the script
main "$@"
 Package Neovim¶
 The script package-neovim.sh is used to compare the latest released version of Neovim to the local version in reprepro.
If out of date, the compressed archive is downloaded, built, packaged into a deb file.
The reason this is separate from update-reprepro.sh is because dependencies need to get packaged along with the binary and an armhf version is not part of the release package.
There are three ways to build the Neovim package for different architectures:
- Docker: Use 
@pve/reprepro/docker/**on the localhost to build for multiple platforms. - Ansible: Use 
@pve/reprepro/ansible/**if you have physical machines with different architectures. - Manual Script: Log into each machine with a different architecture and run the 
@pve/reprepro/package-neovim.shscript. 
Tip
To get multiple architectures of the deb file, the script may be run on different architecture platforms. For instance, I use my RPi2 to build the armv7l, RPi5 to build the arm64, and HP to build the amd64 version.
package-neovim.sh
#!/usr/bin/env bash
################################################################################
#
# Script Name: package-neovim.sh
# ----------------
# Clones, builds, and packages the latest release of Neovim as a .deb file.
#
# @author Nicholas Wilde, 0xb299a622
# @date 16 Oct 2025
# @version 0.2.0
#
################################################################################
# Options
# set -e
# set -o pipefail
# These are constants
readonly BLUE=$(tput setaf 4)
readonly RED=$(tput setaf 1)
readonly YELLOW=$(tput setaf 3)
readonly PURPLE=$(tput setaf 5)
readonly RESET=$(tput sgr0)
readonly SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Default variables
DEBUG="false"
if [ ! -f "${SCRIPT_DIR}/.env" ]; then
  echo "ERRO[$(date +'%Y-%m-%d %H:%M:%S')] The .env file is missing. Please create it." >&2
  exit 1
fi
source "${SCRIPT_DIR}/.env"
# Logging function
function log() {
  local type="$1"
  local color="$RESET"
  if [ "${type}" = "DEBU" ] && [ "${DEBUG}" != "true" ]; then
    return 0
  fi
  case "$type" in
    INFO)
      color="$BLUE";;
    WARN)
      color="$YELLOW";;
    ERRO)
      color="$RED";;
    DEBU)
      color="$PURPLE";;
    *)
      type="LOGS";;
  esac
  if [[ -t 0 ]]; then
    local message="$2"
    echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${message}"
  else
    while IFS= read -r line; do
      echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${line}"
    done
  fi
}
function usage() {
  cat <<EOF
Usage: $0 [options]
Clones, builds, and packages the latest release of Neovim as a .deb file.
Options:
  -d, --debug         Enable debug mode, which prints more info.
  -h, --help          Display this help message.
EOF
}
# Cleanup function to remove temporary directory
function cleanup() {
  if [ -d "${TEMP_PATH}" ]; then
    log "INFO" "Cleaning up temporary directory: ${TEMP_PATH}"
    rm -rf "${TEMP_PATH}"
  fi
}
# Checks if a command exists.
function command_exists() {
  command -v "$1" >/dev/null 2>&1
}
function check_dependencies() {
  if ! command_exists curl || ! command_exists jq || ! command_exists git || ! command_exists make || ! command_exists cpack; then
    log "ERRO" "Required dependencies (curl, jq, git, make, cpack) are not installed."
    exit 1
  fi
}
function make_temp_dir(){
  export TEMP_PATH=$(mktemp -d)
  if [ ! -d "${TEMP_PATH}" ]; then
    log "ERRO" "Could not create temp dir"
    exit 1
  fi
  log "INFO" "Temp path: ${TEMP_PATH}"
}
function get_latest_version() {
  local api_url="https://api.github.com/repos/neovim/neovim/releases/latest"
  local curl_args=('-s')
  if [ -n "${GITHUB_TOKEN}" ]; then
    curl_args+=('-H' "Authorization: Bearer ${GITHUB_TOKEN}")
  fi
  export json_response=$(curl "${curl_args[@]}" "${api_url}")
  if ! echo "${json_response}" | jq -e '.tag_name' >/dev/null 2>&1; then
    log "ERRO" "Failed to get latest version for neovim from GitHub API."
    echo "${json_response}"
    exit 1
  fi
  export TAG_NAME=$(echo "${json_response}" | jq -r '.tag_name')
  log "INFO" "Latest neovim version: ${TAG_NAME}"
}
function main() {
  trap cleanup EXIT
  while [[ "$#" -gt 0 ]]; do
    case $1 in
      -d|--debug) DEBUG="true"; shift;;
      -h|--help) usage; exit 0;;
      *) log "ERRO" "Unknown parameter passed: $1"; usage; exit 1;;
    esac
  done
  log "INFO" "Starting package neovim script..."
  check_dependencies
  make_temp_dir
  log "INFO" "Architecture: $(dpkg --print-architecture)"
  get_latest_version
  local tarball_url
  tarball_url=$(echo "${json_response}" | jq -r '.tarball_url')
  if [ -z "${tarball_url}" ] || [ "${tarball_url}" == "null" ]; then
    log "ERRO" "Could not find tarball URL in GitHub API response."
    echo "${json_response}"
    exit 1
  fi
  log "INFO" "Downloading and extracting neovim version ${TAG_NAME} from ${tarball_url}"
  mkdir -p "${TEMP_PATH}/neovim"
  curl -sL "${tarball_url}" | tar -xz --strip-components=1 -C "${TEMP_PATH}/neovim" 2>&1 | log "INFO"
  cd "${TEMP_PATH}/neovim"
  log "INFO" "Building neovim..."
  make CMAKE_BUILD_TYPE=RelWithDebInfo 2>&1 | log "INFO"
  log "INFO" "Packaging neovim..."
  cd build
  cpack -G DEB 2>&1 | log "INFO"
  local deb_file
  deb_file=$(find . -maxdepth 1 -name "*.deb")
  log "INFO" "Copying ${deb_file} to ${SCRIPT_DIR}"
  cp "${deb_file}" "${SCRIPT_DIR}/"
  log "INFO" "Neovim package created."
  log "INFO" "Debian package: ${TEMP_PATH}/neovim/build/${deb_file}"
  log "INFO" "Script finished."
}
main "$@"
 Package SOPS¶
 The script package-sops.sh is used to compare the latest released version of SOPS to the local version in reprepro.
If out of date, the compressed archive is downloaded, built, packaged into a deb file.
The reason this is separate from update-reprepro.sh is because the sops repo doesn't offer an armhf version and so I manually build and package the armhf version and add it to reprepro.
Tip
To get multiple architectures of the deb file, the script may be run on different architecture platforms. For instance, I use my RPi2 to build the armhf version.
package-sops.sh
#!/usr/bin/env bash
################################################################################
#
# Script Name: package-sops.sh
# ----------------
# Clones, builds, and packages the latest release of sops as a .deb file.
#
# @author Nicholas Wilde, 0xb299a622
# @date 17 Oct 2025
# @version 0.2.0
#
################################################################################
# set -e
# set -o pipefail
# Constants
readonly BLUE=$(tput setaf 4)
readonly RED=$(tput setaf 1)
readonly YELLOW=$(tput setaf 3)
readonly RESET=$(tput sgr0)
readonly SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Logging function
function log() {
  local type="$1"
  # local message="$2"
  local color="$RESET"
  case "$type" in
    INFO)
      color="$BLUE";;
    WARN)
      color="$YELLOW";;
    ERRO)
      color="$RED";;
    # Add a default case for other types
    *)
      type="LOGS";;
  esac
  if [[ -t 0 ]]; then
    local message="$2"
    echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${message}"
  else
    while IFS= read -r line; do
      echo -e "${color}${type}${RESET}[$(date +'%Y-%m-%d %H:%M:%S')] ${line}"
    done
  fi
}
# Cleanup function to remove temporary directory
function cleanup() {
  if [ -d "${TEMP_PATH}" ]; then
    log "INFO" "Cleaning up temporary directory: ${TEMP_PATH}"
    rm -rf "${TEMP_PATH}"
  fi
}
function make_temp_dir(){
  export TEMP_PATH=$(mktemp -d)
  if [ ! -d "${TEMP_PATH}" ]; then
    log "ERRO" "Could not create temp dir"
    exit 1
  fi
  log "INFO" "Temp path: ${TEMP_PATH}"
}
function get_latest_version() {
  local api_url="https://api.github.com/repos/getsops/sops/releases/latest"
  local curl_args=('-s')
  if [ -n "${GITHUB_TOKEN}" ]; then
    curl_args+=('-H' "Authorization: Bearer ${GITHUB_TOKEN}")
  fi
  export json_response=$(curl "${curl_args[@]}" "${api_url}")
  if ! echo "${json_response}" | jq -e '.tag_name' >/dev/null 2>&1; then
    log "ERRO" "Failed to get latest version for sops from GitHub API."
    echo "${json_response}"
    exit 1
  fi
  export TAG_NAME=$(echo "${json_response}" | jq -r '.tag_name')
  export LATEST_VERSION=${TAG_NAME#v}
  log "INFO" "Latest sops version: ${LATEST_VERSION} (tag: ${TAG_NAME})"
}
function get_description() {
  export DESCRIPTION
  DESCRIPTION=$(curl -s "https://api.github.com/repos/getsops/sops" | jq -r '.description' | sed -e 's/:\w\+://g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
}
function check_dependencies() {
  log "INFO" "Checking for dependencies..."
  if command -v go &> /dev/null; then
    export GO_CMD=$(command -v go)
  else
    log "WARN" "Go not found in PATH. Searching common locations..."
    local go_executable=""
    if [ -x "/usr/local/go/bin/go" ]; then
      go_executable="/usr/local/go/bin/go"
    elif [ -n "$SUDO_USER" ]; then
      local user_home
      user_home=$(getent passwd "$SUDO_USER" | cut -d: -f6)
      if [ -x "${user_home}/go/bin/go" ]; then
        go_executable="${user_home}/go/bin/go"
      fi
    fi
    if [ -n "${go_executable}" ]; then
      log "INFO" "Found go at ${go_executable}."
      export GO_CMD="${go_executable}"
      export PATH="$(dirname "${go_executable}"):${PATH}"
    else
      log "ERRO" "Go is not installed or couldn't be found. Please install Go."
      exit 1
    fi
  fi
  if ! command -v dpkg-deb &> /dev/null; then
    log "ERRO" "dpkg-deb is not installed. Please install it."
    exit 1
  fi
  if ! command -v jq &> /dev/null; then
    log "ERRO" "jq is not installed. Please install jq."
    exit 1
  fi
  log "INFO" "All dependencies are installed."
}
function check_root(){
  if [ "$UID" -ne 0 ]; then
    log "ERRO" "Please run as root or with sudo."
    exit 1
  fi
}
function main() {
    trap cleanup EXIT
    check_root
    check_dependencies
    make_temp_dir
    log "INFO" "Starting package sops script..."
    log "INFO" "Building for architecture: armhf"
    get_latest_version
    get_description
    local tarball_url
    tarball_url=$(echo "${json_response}" | jq -r '.tarball_url')
    if [ -z "${tarball_url}" ] || [ "${tarball_url}" == "null" ]; then
      log "ERRO" "Could not find tarball URL in GitHub API response."
      echo "${json_response}"
      exit 1
    fi
    log "INFO" "Downloading and extracting sops version ${TAG_NAME} from ${tarball_url}"
    mkdir -p "${TEMP_PATH}/sops"
    curl -sL "${tarball_url}" | tar -xz --strip-components=1 -C "${TEMP_PATH}/sops" 2>&1 | log "INFO"
    cd "${TEMP_PATH}/sops"
    log "INFO" "Building sops for armhf..."
    GOOS=linux GOARCH=arm ${GO_CMD} build ./cmd/sops 2>&1 | log "INFO"
    local arch_debian="armhf"
    local package_dir="${TEMP_PATH}/sops_${LATEST_VERSION}_${arch_debian}"
    mkdir -p "${package_dir}/usr/local/bin"
    mkdir -p "${package_dir}/DEBIAN"
    mv sops "${package_dir}/usr/local/bin/"
    log "INFO" "Creating control file..."
    cat << EOF > "${package_dir}/DEBIAN/control"
Package: sops
Version: ${LATEST_VERSION}
Section: utils
Priority: optional
Architecture: ${arch_debian}
Maintainer: Nicholas Wilde <[email protected]>
Description: ${DESCRIPTION}
EOF
    log "INFO" "Building .deb package..."
    local deb_file="sops_${LATEST_VERSION}_${arch_debian}.deb"
    if ! dpkg-deb --build "${package_dir}" "${TEMP_PATH}/${deb_file}"; then
        log "ERRO" "Failed to build .deb package for sops ${LATEST_VERSION} ${arch_debian}"
        exit 1
    fi
    log "INFO" "Copying ${deb_file} to ${SCRIPT_DIR}"
    cp "${TEMP_PATH}/${deb_file}" "${SCRIPT_DIR}/"
    log "INFO" "Sops package created: ${SCRIPT_DIR}/${deb_file##*/}"
}
main "$@"
 Upload Deb Files¶
 Once the neovim or sops deb files are built, they are copied to the current pve/reprepro folder. The upload-debs task can then be used to push the deb files to the reprepro LXC using scp.
The REMOTE_IP, REMOTE_USER, and REMOTE_PATH variables in the .env file are used to specify the reprepro LXC.
 Script Notifications¶
 Some scripts can send notifications via Mailrise.
Set the MAILRISE_* variables and the ENABLE_NOTIFICATIONS variable in the .env file.
 Cronjob¶
 A cronjob can be setup to run every night to check the released versions.
2 A.M. nightly
Traefik¶
homelab/pve/traefik/conf.d/reprepro.yaml
 ---
http:
 #region routers 
  routers:
    reprepro:
      entryPoints:
        - "websecure"
      rule: "Host(`deb.l.nicholaswilde.io`)"
      middlewares:
        - default-headers@file
        - https-redirectscheme@file
      tls: {}
      service: reprepro
#endregion
#region services
  services:
    reprepro:
      loadBalancer:
        servers:
          - url: "http://192.168.2.32"
        passHostHeader: true
# #endregion
Task List¶
task: Available tasks for this project:
* bootstrap:                  Bootstrap the reprepro environment
* build-lnav:                 Build lnav
* clear:                      Remove all packages from all distributions
* decrypt:                    Decrypt sensitive configuration files using SOPS.
* deps:                       Install dependencies
* deps-lnav:                  Install lnav dependencies
* dirs:                       Create reprepro directories
* download:                   Download SOPS and Task .deb files
* encrypt:                    Encrypt sensitive configuration files using SOPS.
* export:                     Export the task list
* init:                       Initialize the application's environment and configuration files.
* list:                       List all packages in all distributions
* nuke:                       Nuke the packages
* package-lastpass-cli:       Package the latest lastpass-cli release
* package-neovim:             Package the latest neovim release
* package-sops:               Package the latest sops release
* symlinks:                   Create reprepro symlinks
* update-reprepro:            Downloads application tar.gz and .deb files, packages them as needed, and adds them to a reprepro repository.
* upload-debs:                Upload the deb packages to a remote server