Codeberg Pages publishes whatever sits on a branch named pages. So a deploy is just: build, then force-push the output to that branch. No CI runner, no API tokens — your existing SSH key does the auth.

#!/usr/bin/env bash
# Build a Hugo site and deploy it to Codeberg Pages by force-pushing the
# built public/ directory to the `pages` branch of the pages repo.
set -euo pipefail
cd "$(dirname "$0")/.."

SITE_URL="https://USER.codeberg.page/"
REMOTE="git@codeberg.org:USER/pages.git"
BRANCH="pages"

# Build with the live baseURL so absolute asset links resolve correctly.
hugo --minify --gc --cleanDestinationDir --baseURL "$SITE_URL"

# Publish public/ as a single fresh commit, force-pushed to the pages branch.
# Force-push means the branch always mirrors the latest build — no stale files.
rm -rf public/.git
git -C public init -q
git -C public checkout -q -B "$BRANCH"
git -C public add -A
git -C public \
  -c user.name="deploy" \
  -c user.email="deploy@example.com" \
  commit -q -m "deploy: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
git -C public push -q -f "$REMOTE" "HEAD:${BRANCH}"
rm -rf public/.git

echo "deployed -> $SITE_URL"

Swap USER for your Codeberg user or org. Three things have to be true first:

  • The pages repo is public — git-pages clones it anonymously to publish, so a private repo fails.
  • A Codeberg webhook on the repo has its branch filter set to pages.
  • Your SSH key is registered with Codeberg and can push to the repo.

The fresh-init trick keeps the published branch a single throwaway commit, so content-hashed assets never pile up — and the commit author is whatever you set, not your real git identity.