Building and Publishing Apt Repos to Github Pages

Download, package, and publish software as a apt repo on Github Pages

uv is becoming really useful, but I noticed there is no apt repo/PPA of it. I decided I wanted to try publishing an apt package repository via Github Pages. The LLMs got me started, but there were a lot of little things to work out, so here’s a complete recipe.

Create a new repo and create a file .github/workflows/debian-apt-repo.yml with the content below. This downloads the uv tool as an example, so you will need to adjust those sections.

You will also need to create unencrypted GPG key (commands below), and:

  • Add it to the repository “Settings” -> “Secrets and varitables” -> “Actions”.
    • Create an “environment”, I called mine “Main”.
    • Add a “secret” for “GPG_PRIVATE_KEY” with the contents of the armored (ASCII) private key (the “private.asc” in my example).
    • Once your private key is in github, delete the key off your system. This means that only github has the key.
    • Add a “variable” for “KEY_ID” and paste in the key fingerprint.
    • Save the binary public key to the repo as “pubkey.gpg”.

Also create an “index.html” which will be shown if someone goes to the top of the repo with a browser.

If your build fails, try following this: “First deployment with GITHUB_TOKEN”

Commit these to your repo and push, and in the “Actions” tab it should build and publish the apt repo to https://<YOURNAME>.github.io/<REPONAME>

For an example repo using this, see “linsomniac/uvrepo”

Simon Willison simultaneously published a “recipe for simple site publishing and an example of scraping and publishing an Atom feed”

The Github Workflow

File .github/workflows/debian-apt-repo.yml:

name: Build & Publish Debian Package

on:
  push:
    branches:
      - main

jobs:
  build-and-publish:
    permissions:
      pages: write
      contents: write
    environment: Main
    runs-on: ubuntu-latest
    env:
      DISTRIBUTION: any
      COMPONENT: main
      ARCHITECTURE: amd64
      RELEASE: 1
      GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
      KEY_ID: ${{ vars.KEY_ID }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y reprepro dpkg-dev curl jq gnupg debsigs          

      - name: Import GPG key
        run: |
          echo "$GPG_PRIVATE_KEY" | gpg --batch --import
          echo "$KEY_ID:6:" |  gpg --batch --import-ownertrust --pinentry-mode=loopback          

      - name: Determine latest uv version
        id: get-version
        run: |
          TAG=$(curl -s https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name')
          VERSION=${TAG#v}
          echo "version=$VERSION" >> $GITHUB_OUTPUT          

      - name: Download latest uv binary
        run: |
          DOWNLOAD_URL=$(curl -s https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | select(.name | endswith(".tar.gz")) | .browser_download_url')
          curl -L "$DOWNLOAD_URL" -o uv.tar.gz
          mkdir dl
          tar xf uv.tar.gz -C dl          

      - name: Build Debian package
        run: |
          PKG_VERSION="${{ steps.get-version.outputs.version }}-$RELEASE"
          PKG="uv_${PKG_VERSION}_${ARCHITECTURE}.deb"
          mkdir -p pkg/DEBIAN pkg/usr/bin
          echo "Package: uv" >pkg/DEBIAN/control
          echo "Version: ${PKG_VERSION}" >>pkg/DEBIAN/control
          echo "Section: utils" >>pkg/DEBIAN/control
          echo "Priority: optional" >>pkg/DEBIAN/control
          echo "Architecture: ${ARCHITECTURE}" >>pkg/DEBIAN/control
          echo "Maintainer: GitHub Actions <actions@github.com>" >>pkg/DEBIAN/control
          echo "Description: UV CLI binary" >>pkg/DEBIAN/control
          fakeroot bash -c "install -o root -g root -m 755 dl/*/uv* pkg/usr/bin ; dpkg-deb --build pkg $PKG"          

      - name: Sign Debian package
        run: |
                    debsigs -v --gpgopts="--batch --no-tty --pinentry-mode=loopback" --sign=origin --default-key="$KEY_ID" uv*.deb

      - name: Create APT repository with reprepro
        run: |
          mkdir -p repo/conf
          echo "Origin: GitHub" >repo/conf/distributions
          echo "Label: GitHub UV" >>repo/conf/distributions
          echo "Suite: stable" >>repo/conf/distributions
          echo "Codename: ${DISTRIBUTION}" >>repo/conf/distributions
          echo "Components: ${COMPONENT}" >>repo/conf/distributions
          echo "Architectures: ${ARCHITECTURE}" >>repo/conf/distributions
          echo "SignWith: $KEY_ID" >>repo/conf/distributions
          reprepro -Vb repo includedeb ${DISTRIBUTION} "uv_${{ steps.get-version.outputs.version }}-${RELEASE}_${ARCHITECTURE}.deb"
          cp pubkey.gpg repo/pubkey.gpg
          cp index.html repo/          

      - name: Deploy APT repository to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./repo

Creating the GPG Key

rm -rf repo-key ; mkdir repo-key ; chmod 700 repo-key
echo "batch" >repo-key/gpg.conf
echo "pinentry-mode loopback" >>repo-key/gpg.conf
gpg --full-generate-key --homedir repo-key --passphrase ''
gpg --list-keys --with-keygrip --homedir repo-key
gpg --homedir repo-key --armor --export-secret-keys [EMAIL_ADDR] >private.asc
gpg --homedir repo-key --export [EMAIL_ADDR] >public.gpg