As promised on my previous post, on this entry I’ll explain how I’ve set up forgejo actions on the source repository of this site to build it using a runner instead of doing it on the public server using a webhook to trigger the operation.
Setting up the system
The first thing I’ve done is to disable the forgejo webhook call that was used to publish the site, as I don’t want to run it anymore.
After that I added a new workflow to the repository that does the following things:
- build the site using my hugo-adoc image.
- push the result to a branch that contains the generated site (we do this because the server is already configured to work with the git repository and we can use force pushes to keep only the last version of the site, removing the need of extra code to manage package uploads and removals).
- uses
curl
to send a notification to an instance of the webhook server installed on the remote server that triggers a script that updates the site using the git branch.
Setting up the webhook
service
On the server machine we have installed and configured the webhook
service to run a script that updates the site.
To install the application and setup the configuration we have used the following script:
#!/bin/sh
set -e
# ---------
# VARIABLES
# ---------
ARCH="$(dpkg --print-architecture)"
WEBHOOK_VERSION="2.8.2"
DOWNLOAD_URL="https://github.com/adnanh/webhook/releases/download"
WEBHOOK_TGZ_URL="$DOWNLOAD_URL/$WEBHOOK_VERSION/webhook-linux-$ARCH.tar.gz"
WEBHOOK_SERVICE_NAME="webhook"
# Files
WEBHOOK_SERVICE_FILE="/etc/systemd/system/$WEBHOOK_SERVICE_NAME.service"
WEBHOOK_SOCKET_FILE="/etc/systemd/system/$WEBHOOK_SERVICE_NAME.socket"
WEBHOOK_TML_TEMPLATE="/srv/blogops/action/webhook.yml.envsubst"
WEBHOOK_YML="/etc/webhook.yml"
# Config file values
WEBHOOK_USER="$(id -u)"
WEBHOOK_GROUP="$(id -g)"
WEBHOOK_LISTEN_STREAM="172.31.31.1:4444"
# ----
# MAIN
# ----
# Install binary from releases (on Debian only version 2.8.0 is available, but
# I need the 2.8.2 version to support the systemd activation mode).
curl -fsSL -o "/tmp/webhook.tgz" "$WEBHOOK_TGZ_URL"
tar -C /tmp -xzf /tmp/webhook.tgz
sudo install -m 755 "/tmp/webhook-linux-$ARCH/webhook" /usr/local/bin/webhook
rm -rf "/tmp/webhook-linux-$ARCH" /tmp/webhook.tgz
# Service file
sudo sh -c "cat >'$WEBHOOK_SERVICE_FILE'" <<EOF
[Unit]
Description=Webhook server
[Service]
Type=exec
ExecStart=webhook -nopanic -hooks $WEBHOOK_YML
User=$WEBHOOK_USER
Group=$WEBHOOK_GROUP
EOF
# Socket config
sudo sh -c "cat >'$WEBHOOK_SOCKET_FILE'" <<EOF
[Unit]
Description=Webhook server socket
[Socket]
# Set FreeBind to listen on missing addresses (the VPN can be down sometimes)
FreeBind=true
# Set ListenStream to the IP and port you want to listen on
ListenStream=$WEBHOOK_LISTEN_STREAM
[Install]
WantedBy=multi-user.target
EOF
# Config file
BLOGOPS_TOKEN="$(uuid)" \
envsubst <"$WEBHOOK_TML_TEMPLATE" | sudo sh -c "cat >$WEBHOOK_YML"
chmod 0640 "$WEBHOOK_YML"
chwon "$WEBHOOK_USER:$WEBHOOK_GROUP" "$WEBHOOK_YML"
# Restart and enable service
sudo systemctl daemon-reload
sudo systemctl stop "$WEBHOOK_SERVICE_NAME.socket"
sudo systemctl start "$WEBHOOK_SERVICE_NAME.socket"
sudo systemctl enable "$WEBHOOK_SERVICE_NAME.socket"
# ----
# vim: ts=2:sw=2:et:ai:sts=2
As seen on the code, we’ve installed the application using a binary from the project repository instead of a package
because we needed the latest version of the application to use systemd
with socket activation.
The configuration file template is the following one:
- id: "update-blogops"
execute-command: "/srv/blogops/action/bin/update-blogops.sh"
command-working-directory: "/srv/blogops"
trigger-rule:
match:
type: "value"
value: "$BLOGOPS_TOKEN"
parameter:
source: "header"
name: "X-Blogops-Token"
The version on /etc/webhook.yml
has the BLOGOPS_TOKEN
adjusted to a random value that has to exported as a secret on
the forgejo project (see later).
Once the service is started each time the action is executed the webhook
daemon will get a notification and will run
the following update-blogops.sh
script to publish the updated version of the site:
#!/bin/sh
set -e
# ---------
# VARIABLES
# ---------
# Values
REPO_URL="ssh://git@forgejo.mixinet.net/mixinet/blogops.git"
REPO_BRANCH="html"
REPO_DIR="public"
MAIL_PREFIX="[BLOGOPS-UPDATE-ACTION] "
# Address that gets all messages, leave it empty if not wanted
MAIL_TO_ADDR="blogops@mixinet.net"
# Directories
BASE_DIR="/srv/blogops"
PUBLIC_DIR="$BASE_DIR/$REPO_DIR"
NGINX_BASE_DIR="$BASE_DIR/nginx"
PUBLIC_HTML_DIR="$NGINX_BASE_DIR/public_html"
ACTION_BASE_DIR="$BASE_DIR/action"
ACTION_LOG_DIR="$ACTION_BASE_DIR/log"
# Files
OUTPUT_BASENAME="$(date +%Y%m%d-%H%M%S.%N)"
ACTION_LOGFILE_PATH="$ACTION_LOG_DIR/$OUTPUT_BASENAME.log"
# ---------
# Functions
# ---------
action_log() {
echo "$(date -R) $*" >>"$ACTION_LOGFILE_PATH"
}
action_check_directories() {
for _d in "$ACTION_BASE_DIR" "$ACTION_LOG_DIR"; do
[ -d "$_d" ] || mkdir "$_d"
done
}
action_clean_directories() {
# Try to remove empty dirs
for _d in "$ACTION_LOG_DIR" "$ACTION_BASE_DIR"; do
if [ -d "$_d" ]; then
rmdir "$_d" 2>/dev/null || true
fi
done
}
mail_success() {
to_addr="$MAIL_TO_ADDR"
if [ "$to_addr" ]; then
subject="OK - updated blogops site"
mail -s "${MAIL_PREFIX}${subject}" "$to_addr" <"$ACTION_LOGFILE_PATH"
fi
}
mail_failure() {
to_addr="$MAIL_TO_ADDR"
if [ "$to_addr" ]; then
subject="KO - failed to update blogops site"
mail -s "${MAIL_PREFIX}${subject}" "$to_addr" <"$ACTION_LOGFILE_PATH"
fi
exit 1
}
# ----
# MAIN
# ----
ret="0"
# Check directories
action_check_directories
# Go to the base directory
cd "$BASE_DIR"
# Remove the old build dir if present
if [ -d "$PUBLIC_DIR" ]; then
rm -rf "$PUBLIC_DIR"
fi
# Update the repository checkout
action_log "Updating the repository checkout"
git fetch --all >>"$ACTION_LOGFILE_PATH" 2>&1 || ret="$?"
if [ "$ret" -ne "0" ]; then
action_log "Failed to update the repository checkout"
mail_failure
fi
# Get it from the repo branch & extract it
action_log "Downloading and extracting last site version using 'git archive'"
git archive --remote="$REPO_URL" "$REPO_BRANCH" "$REPO_DIR" \
| tar xf - >>"$ACTION_LOGFILE_PATH" 2>&1 || ret="$?"
# Fail if public dir was missing
if [ "$ret" -ne "0" ] || [ ! -d "$PUBLIC_DIR" ]; then
action_log "Failed to download or extract site"
mail_failure
fi
# Remove old public_html copies
action_log 'Removing old site versions, if present'
find $NGINX_BASE_DIR -mindepth 1 -maxdepth 1 -name 'public_html-*' -type d \
-exec rm -rf {} \; >>"$ACTION_LOGFILE_PATH" 2>&1 || ret="$?"
if [ "$ret" -ne "0" ]; then
action_log "Removal of old site versions failed"
mail_failure
fi
# Switch site directory
TS="$(date +%Y%m%d-%H%M%S)"
if [ -d "$PUBLIC_HTML_DIR" ]; then
action_log "Moving '$PUBLIC_HTML_DIR' to '$PUBLIC_HTML_DIR-$TS'"
mv "$PUBLIC_HTML_DIR" "$PUBLIC_HTML_DIR-$TS" >>"$ACTION_LOGFILE_PATH" 2>&1 ||
ret="$?"
fi
if [ "$ret" -eq "0" ]; then
action_log "Moving '$PUBLIC_DIR' to '$PUBLIC_HTML_DIR'"
mv "$PUBLIC_DIR" "$PUBLIC_HTML_DIR" >>"$ACTION_LOGFILE_PATH" 2>&1 ||
ret="$?"
fi
if [ "$ret" -ne "0" ]; then
action_log "Site switch failed"
mail_failure
else
action_log "Site updated successfully"
mail_success
fi
# ----
# vim: ts=2:sw=2:et:ai:sts=2
The hugo-adoc
workflow
The workflow is defined in the .forgejo/workflows/hugo-adoc.yml
file and looks like this:
name: hugo-adoc
# Run this job on push events to the main branch
on:
push:
branches:
- 'main'
jobs:
build-and-push:
if: ${{ vars.BLOGOPS_WEBHOOK_URL != '' && secrets.BLOGOPS_TOKEN != '' }}
runs-on: docker
container:
image: forgejo.mixinet.net/oci/hugo-adoc:latest
# Allow the job to write to the repository (not really needed on forgejo)
permissions:
contents: write
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
submodules: 'true'
- name: Build the site
shell: sh
run: |
rm -rf public
hugo
- name: Push compiled site to html branch
shell: sh
run: |
# Set the git user
git config --global user.email "blogops@mixinet.net"
git config --global user.name "BlogOps"
# Create a new orphan branch called html (it was not pulled by the
# checkout step)
git switch --orphan html
# Add the public directory to the branch
git add public
# Commit the changes
git commit --quiet -m "Updated site @ $(date -R)" public
# Push the changes to the html branch
git push origin html --force
# Switch back to the main branch
git switch main
- name: Call the blogops update webhook endpoint
shell: sh
run: |
HEADER="X-Blogops-Token: ${{ secrets.BLOGOPS_TOKEN }}"
curl --fail -k -H "$HEADER" ${{ vars.BLOGOPS_WEBHOOK_URL }}
The only relevant thing is that we have to add the BLOGOPS_TOKEN
variable to the project secrets (its value is the one
included on the /etc/webhook.yml
file created when installing the webhook
service) and the BLOGOPS_WEBHOOK_URL
project variable (its value is the URL of the webhook
server, in my case
http://172.31.31.1:4444/hooks/update-blogops
); note that the job includes the -k
flag on the curl
command just in
case I end up using TLS on the webhook
server in the future, as discussed previously.
Conclusion
Now that I have forgejo actions on my server I no longer need to build the site on the public server as I did initially, a good thing when the server is a small OVH VPS that only runs a couple of containers and a web server directly on the host.
I’m still using a notification system to make the server run a script to update the site because that way the forgejo
server does not need access to the remote machine shell, only the webhook
server which, IMHO, is a more secure setup.