Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions .github/workflows/apt-repo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Update APT Repository

on:
release:
types: [published]

# Allow manual trigger for re-processing a release
workflow_dispatch:
inputs:
release_tag:
description: "Release tag to process (e.g. v1.0.0)"
required: true

permissions:
contents: write

jobs:
update-apt-repo:
runs-on: ubuntu-latest
steps:
- name: Determine release tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
fi

- name: Install APT repository tools
run: |
sudo apt-get update
sudo apt-get install -y dpkg-dev gzip

# GPG_SIGNING: Uncomment to import the GPG key for signing repository metadata.
# Requires the following GitHub secrets:
# GPG_PRIVATE_KEY - ASCII-armored private key (gpg --armor --export-secret-keys KEY_ID)
# GPG_PASSPHRASE - Passphrase for the private key
# GPG_KEY_ID - Key ID or email used to identify the signing key
#
# - name: Import GPG key
# run: |
# echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
# echo "allow-preset-passphrase" >> ~/.gnupg/gpg-agent.conf
# gpgconf --kill gpg-agent
# gpgconf --launch gpg-agent

- name: Checkout gh-pages branch
uses: actions/checkout@v4
with:
ref: gh-pages
path: gh-pages

- name: Download .deb files from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p debs
cd debs
gh release download "${{ steps.tag.outputs.tag }}" \
--repo "${{ github.repository }}" \
--pattern "*.deb"
echo "Downloaded .deb files:"
ls -la *.deb

- name: Build APT repository metadata
run: |
# Set up the pool directory structure.
# All .deb files live under pool/main/ so APT can fetch them.
mkdir -p gh-pages/pool/main
cp debs/*.deb gh-pages/pool/main/

# Generate architecture-specific Packages indexes.
for arch in amd64 arm64; do
dir="gh-pages/dists/stable/main/binary-${arch}"
mkdir -p "${dir}"

# Scan the pool for .deb files matching this architecture and
# produce the Packages index. dpkg-scanpackages paths are relative
# to the repository root (gh-pages/).
(cd gh-pages && dpkg-scanpackages --arch "${arch}" pool/main) \
> "${dir}/Packages"

gzip -9c "${dir}/Packages" > "${dir}/Packages.gz"

echo "Packages index for ${arch}:"
cat "${dir}/Packages"
done

- name: Generate Release file
run: |
release_file="gh-pages/dists/stable/Release"
{
echo "Origin: observIQ"
echo "Label: bindplane-supervisor"
echo "Suite: stable"
echo "Codename: stable"
echo "Architectures: amd64 arm64"
echo "Components: main"
echo "Date: $(date -Ru)"
echo "Description: APT repository for bindplane-supervisor"

# Checksums cover every Packages / Packages.gz file under dists/.
# APT uses these to verify integrity of downloaded index files.
echo "MD5Sum:"
(cd gh-pages/dists/stable && find . -name 'Packages*' -type f | sort | while read -r file; do
size=$(stat -c%s "${file}")
md5=$(md5sum "${file}" | awk '{print $1}')
# Strip leading "./" to get the relative path APT expects.
printf " %s %16d %s\n" "${md5}" "${size}" "${file#./}"
done)

echo "SHA256:"
(cd gh-pages/dists/stable && find . -name 'Packages*' -type f | sort | while read -r file; do
size=$(stat -c%s "${file}")
sha256=$(sha256sum "${file}" | awk '{print $1}')
printf " %s %16d %s\n" "${sha256}" "${size}" "${file#./}"
done)
} > "${release_file}"

echo "Release file contents:"
cat "${release_file}"

# GPG_SIGNING: Uncomment to create InRelease (inline-signed) and Release.gpg
# (detached signature). APT uses these to verify the repository is authentic.
#
# - name: Sign Release file
# run: |
# # InRelease: clearsigned version of Release (preferred by modern APT)
# gpg --batch --yes --pinentry-mode loopback \
# --passphrase "${{ secrets.GPG_PASSPHRASE }}" \
# --default-key "${{ secrets.GPG_KEY_ID }}" \
# --clearsign \
# -o gh-pages/dists/stable/InRelease \
# gh-pages/dists/stable/Release
#
# # Release.gpg: detached signature (fallback for older APT versions)
# gpg --batch --yes --pinentry-mode loopback \
# --passphrase "${{ secrets.GPG_PASSPHRASE }}" \
# --default-key "${{ secrets.GPG_KEY_ID }}" \
# --detach-sign --armor \
# -o gh-pages/dists/stable/Release.gpg \
# gh-pages/dists/stable/Release
#
# - name: Export public key for users
# run: |
# # Publish the public key so users can add it to their APT keyring.
# gpg --armor --export "${{ secrets.GPG_KEY_ID }}" \
# > gh-pages/signing-key.gpg

- name: Commit and push to gh-pages
working-directory: gh-pages
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Update APT repository for ${{ steps.tag.outputs.tag }}"
git push
65 changes: 65 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Installing bindplane-supervisor via APT

## Prerequisites

- A Debian-based Linux distribution (Debian, Ubuntu, etc.)
- `curl` and root/sudo access

> **Note:** GPG signing is not currently enabled for this repository. The
> instructions below use `[trusted=yes]` to allow APT to fetch packages without
> signature verification. Once GPG signing is enabled this will be replaced with
> proper key verification.

## Add the APT Repository

```bash
# Add the repository source list.
# Replace OWNER with the GitHub org or user (e.g. observIQ).
echo "deb [trusted=yes] https://OWNER.github.io/bindplane-supervisor/dists/stable/ stable main" \
| sudo tee /etc/apt/sources.list.d/bindplane-supervisor.list > /dev/null
```

<!-- GPG_SIGNING: When GPG signing is enabled, replace the block above with:
```bash
# Download the repository signing key.
curl -fsSL https://OWNER.github.io/bindplane-supervisor/signing-key.gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/bindplane-supervisor.gpg

# Add the repository with keyring-based verification.
echo "deb [signed-by=/usr/share/keyrings/bindplane-supervisor.gpg] https://OWNER.github.io/bindplane-supervisor/dists/stable/ stable main" \
| sudo tee /etc/apt/sources.list.d/bindplane-supervisor.list > /dev/null
```
-->

## Install

```bash
sudo apt-get update
sudo apt-get install bindplane-supervisor
```

## Update

```bash
sudo apt-get update
sudo apt-get upgrade bindplane-supervisor
```

## Install a Specific Version

```bash
# List available versions.
apt-cache madison bindplane-supervisor

# Install a specific version.
sudo apt-get install bindplane-supervisor=<version>
```

## Uninstall

```bash
sudo apt-get remove bindplane-supervisor

# To also remove configuration files:
sudo apt-get purge bindplane-supervisor
```
166 changes: 166 additions & 0 deletions README_APT_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# APT Repository Setup

This project uses **GitHub Pages** to host APT repository metadata and
**GitHub Releases** to store the actual `.deb` packages. GoReleaser builds
`.deb` files on every tagged release, and a GitHub Actions workflow
(`.github/workflows/apt-repo.yml`) regenerates the APT metadata automatically.

## Architecture

```
GitHub Release (v1.2.3)
└─ bindplane-supervisor_v1.2.3_linux_amd64.deb
└─ bindplane-supervisor_v1.2.3_linux_arm64.deb

gh-pages branch (served by GitHub Pages)
├─ pool/main/ ← .deb files copied here
│ ├─ bindplane-supervisor_v1.2.3_linux_amd64.deb
│ └─ bindplane-supervisor_v1.2.3_linux_arm64.deb
├─ dists/stable/
│ ├─ Release ← repository-level metadata
│ ├─ main/
│ │ ├─ binary-amd64/
│ │ │ ├─ Packages
│ │ │ └─ Packages.gz
│ │ └─ binary-arm64/
│ │ ├─ Packages
│ │ └─ Packages.gz
│ # After GPG signing is enabled:
│ # ├─ InRelease ← inline-signed Release
│ # └─ Release.gpg ← detached Release signature
└─ # signing-key.gpg ← public GPG key for users
```

**How it works:**

1. A developer pushes a tag (`v1.2.3`).
2. The `release.yml` workflow runs GoReleaser, which builds and uploads `.deb`
files to the GitHub Release.
3. The `apt-repo.yml` workflow triggers on release publication, downloads the
`.deb` files, generates APT metadata, and pushes to `gh-pages`.
4. GitHub Pages serves the `gh-pages` branch, making the APT repo available at
`https://<OWNER>.github.io/bindplane-supervisor/`.

## Initial Setup

### 1. Create the gh-pages branch

The `gh-pages` branch must exist before the workflow can push to it. Create it
as an empty orphan branch:

```bash
git checkout --orphan gh-pages
git rm -rf .
echo "APT repository for bindplane-supervisor" > index.html
git add index.html
git commit -m "Initialize gh-pages branch"
git push origin gh-pages
git checkout main
```

### 2. Enable GitHub Pages

1. Go to the repository **Settings > Pages**.
2. Under **Source**, select **Deploy from a branch**.
3. Set the branch to `gh-pages` and the folder to `/ (root)`.
4. Click **Save**.

The repository will be available at
`https://<OWNER>.github.io/bindplane-supervisor/` once the first workflow run
populates it.

### 3. Verify

After the next release (or a manual workflow dispatch), confirm:

```bash
curl -s https://<OWNER>.github.io/bindplane-supervisor/dists/stable/Release
```

You should see the `Release` file contents with `Origin`, `Label`, checksums,
etc.

## Adding GPG Signing

GPG signing lets users verify that the repository metadata has not been
tampered with. The workflow and install instructions already contain
commented-out GPG sections marked with `GPG_SIGNING:`.

### Step 1 — Generate a GPG key (if you don't have one)

```bash
gpg --full-generate-key
# Choose RSA (sign only), 4096 bits, no expiration (or set one).
# Note the key ID from the output.
```

### Step 2 — Add GitHub secrets

Go to **Settings > Secrets and variables > Actions** and add:

| Secret | Value |
|--------------------|--------------------------------------------------------------|
| `GPG_PRIVATE_KEY` | `gpg --armor --export-secret-keys <KEY_ID>` |
| `GPG_PASSPHRASE` | Passphrase for the key |
| `GPG_KEY_ID` | Key ID or email (e.g. `ABCDEF1234567890` or `you@example.com`) |

### Step 3 — Uncomment GPG sections

In `.github/workflows/apt-repo.yml`, find every block prefixed with
`# GPG_SIGNING:` and uncomment the steps below it:

- **Import GPG key** — imports the private key into the runner's keyring.
- **Sign Release file** — generates `InRelease` and `Release.gpg`.
- **Export public key for users** — writes `signing-key.gpg` for users.

In `INSTALL.md`, swap the `[trusted=yes]` source list entry for the
`[signed-by=...]` entry inside the `<!-- GPG_SIGNING: ... -->` comment block.

### Step 4 — Test

Trigger a manual workflow run and verify:

```bash
# The InRelease file should exist and contain a PGP signature.
curl -s https://<OWNER>.github.io/bindplane-supervisor/dists/stable/InRelease | head -5

# The public key should be downloadable.
curl -fsSL https://<OWNER>.github.io/bindplane-supervisor/signing-key.gpg | gpg --show-keys
```

## Troubleshooting

### `apt-get update` returns 404

- Confirm GitHub Pages is enabled and serving from `gh-pages`.
- Confirm the `gh-pages` branch contains `dists/stable/Release`.
- The sources list URL must **not** include a trailing slash after the hostname
path.

### `apt-get update` shows "The repository is not signed"

This is expected while GPG signing is disabled. Using `[trusted=yes]` in the
source list suppresses this error. Follow the GPG signing steps above to
resolve it permanently.

### Workflow fails with "no .deb files found"

- Confirm GoReleaser is configured to produce `.deb` packages (check the
`nfpms` section in `.goreleaser.yaml`).
- Check the GitHub Release assets to verify `.deb` files were uploaded.
- If the release was created as a draft, `.deb` files may not be available
until the release is published.

### Old package versions not available

Every workflow run copies all `.deb` files to `pool/main/` and regenerates the
index. Older versions remain available as long as their `.deb` files stay in
`pool/main/` on the `gh-pages` branch.

### Workflow needs to be re-run for an existing release

Use the manual workflow dispatch:

1. Go to **Actions > Update APT Repository**.
2. Click **Run workflow**.
3. Enter the release tag (e.g. `v1.2.3`).