Von CGI Zu Forgejo Action
Da ich vor kurzen die Action in Forgejo gefunden habe, musste ich diese nun direkt ausprobieren. Und das automatisierte erstellen meiner Webseite, dass aktuell über hooks und CGI durchgeführt wurde und immer wieder Probleme macht, war das ideale Testobjekt.
Installation
Bei Go-Programmen sehe ich den Nutzen von docker nicht, daher habe ich den forgejo-runner auch diesmal direkt als
einzelne Datei heruntergeladen und als Service in meinen Server hinterlegt. Zum installieren und aktualisieren verwende
ich das
Script
, mit folgenden Konfigurationen:
target_architecture="linux-amd64"
prog_installdir="/usr/local/bin"
git_server="code.forgejo.org"
git_repo="forgejo/runner"
prog_link_name="ci-runner"
prog_service="ci-runner.service"
Einrichten
Beim einrichten des Runners, habe ich mich sehr an die originale Dokumentation von forgejo gehalten. Diese findet man hier .
Wichtig dabei ist, dass ein eigener Benutzer für den Runner erzeugt wird. Unter diesen liegen dann die Konfigurationen und er ist entsprechend für die Läufe abgeschottet. Die Konfigurationsdatei kann direkt von Runner mit dem nachfolgenden Aufruf erzeugt werden und im Home-Verzeichnis abgelegt werden:
ci-runner generate-config > /home/runner/config.yml
Sinnvoll ist die Implementierung als systemd-Dienst, dafür kann das Script am Ende genommen werden. Meine Variante schaut wie folgt aus, wobei mein Benutzername runner benannt wurde:
[Unit]
Description=Forgejo Runner
Documentation=https://forgejo.org/docs/latest/admin/actions/
After=gitea.service
[Service]
ExecStart=ci-runner --config /home/runner/config.yml daemon
ExecReload=/bin/kill -s HUP $MAINPID
# This user and working directory must already exist
User=runner
WorkingDirectory=/home/runner
Restart=on-failure
TimeoutSec=0
RestartSec=10
[Install]
WantedBy=mutli-user.target
Registrieren
Zum registrieren des Runners muss zuerst ein Runner erstellt werden. Die einfachste Variante ist das über das Einstellungsmenü, welches auf der linken Seite unter dem Menüpunkt Action den Untermenüpunkt Runner besitzt. Dort wird nun ein neuer Runner erstellt und die Informationen können nun direkt in der Konfigurationsdatei hinterlegt werden, wie es in der offiziellen Doku [hier][FCIR] beschrieben ist.
Die Registrierung über die Kommandozeile am runner kann weiterhin verwendet werden, aber ist aufwendiger und würde ich bei einer neuen Installation auch nicht mehr durchführen.
Repository
Zuerst sollte das Repository überprüft werden. Hierbei ist wichtig, dass die Submodule so verlinkt sind, dass der Server
auf dem der Runner läuft entsprechenden Zugriff besitzt. Am einfachsten ist dies, wenn die Verlinkung über das
http-Protokoll durchgeführt wird. Bei ssh müssten die known_hosts-Einträge hinterlegt werden und ein privater
Schlüssel für den Zugriff.
Um die Konfiguration direkt zu testen, kann der nachfolgende Befehl ausgeführt werden:
git clone --recurse-submodule https://gitea.example.com/myname/myproject
ci-runner exec -c ~/config.yml -i -self-hosted
Der Clone-Befehl kann ignoriert werden, wenn das Repository auf dem aktuellen Rechner vorliegt.
Konfiguration
Die Konfiguration für den Runner wird direkt im Repository abgelegt. Dafür wird die Verzeichnisstruktur .forgejo/workflows
erzeugt. In diese werden die .yml-Dateien abgelegt. Hier können je nach Bedarf auch mehr als eine Datei erstellt werden.
Vorbereitungen
Um die Scripts von lokalen Gegebenheiten frei zu halten, gibt es Variablen und Geheiminisse die entweder direkt am Projekt definiert werden können, oder global für den kompletten Server. Für beide müssen die jeweiligen Einstellungen aufgerufen werden. Auf der linken Seite gibt es den Menüpunkt Actions unter dem die Unterpunkte für die Variablen und Geheimnisse liegen.
Unter Varialben können Werte wie Servernamen oder Verzeichnisse hinterlegt werden. Diese Werte können von jedem mit Zugriff angeschaut und verändert werden. Der Zugriff in den Scripten wird mit dem Präfix vars. gesteuert.
Die Geheimnisse sind für sinnvoll für private Schlüssel oder Passwörter. Diese Werte können außerhalb der Scripte nicht ausgelesen werden sondern nur noch verändert werden. Der Zugriff in den Scripten wird mit dem Präfix secret. gesteuert.
Der Anfang
Der Anfang dieser Datei schaut wie folgt aus:
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
buildanddeploy:
name: ${{ matrix.builds.name }}-site - build and deploy
runs-on: self-hosted
strategy:
matrix:
builds:
- name: develop
folder: ${{ vars.DEV_FOLDER }}
buildarg: -D -E -F
createcommit: yes
- name: production
folder: ${{ vars.PROD_FOLDER }}
buildarg: --minify
createcommit: yes
steps:
- ...
Mit dem Schlüsselwort on wird definiert auf welche Aktion hin, dieser Auftrag ausfgeführt werden soll. In diesem Fall wird die Aktion ausgeführt, wenn auf den Branch main neue Daten gepusht werden.
Mit dem Schlüsselwort jobs werden die Aufträge und ihre Schritte definiert. Zuerst wird der Name des Auftrags angegeben. Dieser Fall verwendet eine Ersetzungs-Variable, da hier ein Mehrfach-Build definiert ist, der durch die strategy/matrix/builds Definition mit Werten gefüllt wird. Der Eintrag runs-on definiert auf welchen Typ von Runner der Auftrag ausgeführt werden soll. Bei den Mehrfach-Bilds ist darauf zu achten, dass die gleichen Parameter vergeben werden, diese werden hier verwendet, um zum einen die Ziel-Ordner unterschiedlich anzugeben. Ebenso wird es genutzt um die unterschiedlichen Aufrufparameter zur Erstellung der Website zu definieren. In meinen Fall verwende ich es um eine Hauptseite und eine Testseite, in der alle Drafts mit erzeugt werden, zu erstellen und zu publischen.
Mit steps wird der Ablauf innerhalb des Auftrags definiert, auf welchen im Nachfolgenden eingegangen wird.
Auschecken
Der erste Schritt ist häufig das Clonen der Quellen vom Source-Code, welches mit folgenden Schritt definiert wird:
- uses: actions/checkout@v4
with:
submodules: true # Fetch Hugo themes (true or recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and Lastmod
Hugo installieren (falls notwendig)
Die Programme Go und Hugo werden durch die Packet-Verwaltung des Server installiert und auf dem aktuellen Stand
gehalten. Dadurch entfallen diese Aktionen in meinen Skript. Sollte das bei euch nicht so sein, dann müsste ihr die
Aktion
peaceiris/actions-hugo@v3
noch zusätzlich nach dem Checkout und for dem Erstellen einfügen, wie hier
zu sehen:
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-Version: 'latest'
extended: true
Zusätzliche Infos erzeugen
Der Schritt definiert das aktuelle Datum als Ausgabe-Wert, welcher im nachfolgenden Script verwendet werden kann. Dafür muss der Schritt mit der hinterlegten id angesprochen werden. In diesem Fall würde der Zugriff so aussehen “${{ steps.date.outputs.date }}”.
Da ich gerne den aktuellen Commit-Hash auf meiner Seite noch mit anzeigen möchte und kann, gibt es dafür einen eigenen Schritt, der diesen in die dafür vorgesehene Datei abgelegt.
- name: Get current date
id: date
run: echo "date=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
- name: Create commit id
if: matrix.builds.createcommit == 'yes'
run: |
echo "Set Commit-ID to ${{ github.SHA }}"
echo "${{ github.SHA }}" > commit_id.txt
Webseite erstellen
Nun wir die Webseite erstellt. Hier wird auch mit den entsprechenden Ersetzungsvariablen aus dem Matrix-Build gearbeitet damit die unterschiedlichen Definitionen gebaut werden können:
- name: build
run: |
echo "Build site with ${{ matrix.builds.buildarg }}"
hugo ${{ matrix.builds.buildarg }}
Upload
Nun müssen die erstellten Webseiten noch auf dem Webserver hochgeladen werden. Hierfür kann ich 2 Möglichkeiten vorstellen.
Upload über FTP
Für den Upload mit ftp, wird hier eine netrc-Datei erzeugt. Somit ist eine Verbindung ohne Passwort-Eingabe möglich. Da hier das Passwort im Klartext hinterlegt wird, wird im Anschluss die Datei sofort wieder entfernt.
- name: setup ftp
run: |
touch ~/.netrc
chmod 0600 ~/.netrc
echo "machine ${{ vars.WEBSERVER_FTP }}" > ~/.netrc
echo "login ${{ vars.WEBSERVER_FTP_LOGIN }}" > ~/.netrc
echo "password ${{ secrets.WEBSERVER_FTP_PASSWORD }}" > ~/.netrc
- name: push public
run: |
ftp ${{ vars.WEBSERVER_FTP }} <<EOF
binary
mkdir ${{ matrix.builds.folder }}
cd ${{ matrix.builds.folder }}
mput ./public/*
bye
EOF
- name: cleanup
run: rm ~/.netrc
Das ablegen in eigene Unterordner pro Build wär hier auch angedacht. Da eine Verlinkung im Nachgang über ftp nicht möglich war, ist die einzige Möglichkeit den Upload direkt in das Hauptverzeichnis durchzuführen.
Upload über SFTP
Und hier ein Beispiel wie es über Secure FTP durchgeführt werden kann.
Hierfür wurde zuerst das aktuelle Datum ermittelt und zurückgegeben, damit dieser Wert im Nachgang für die Erstellung der dynamischen Zielordner verwendet werden kann.
Der zweite Schritt ist das vorbereiten für die SSH Verbindung. Hierbei wird das hinterlegte Geheimniss als privater Schlüssel zur Anmeldung erzeugt, und der externe SSH-Server und sein Öffentlicher Schlüssel als bekannter Computer hinterlegt.
Im dritten Schritt werden nun die Dateien in den dynamischen Ordner hochgeladen. Danach wird der symlink neu verknüpft, wodurch die Downtime minimal gehalten wird.
Im letzten Schritt werden dann zur Sicherheit noch die ssh-Schlüssel wieder entfernt.
Eine Verwendung von scp hat leider nicht funktioniert, da diese keine Zielordner beim Upload erzeugen kann. Da dies nur via sftp funktioniert, habe ich mich dazu entschlossen nun alles direkt über dies zu implementieren.
- name: Get current date
id: date
run: echo "date=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
- name: setup ssh
run: |
echo "Generate PrivKey und KnownHost-Entry for ${{ vars.SERVER_NAME }}"
mkdir -p ~/.ssh
chmod 0700 ~/.ssh
echo "${{ secrets.SERVER_SFTP_PRIVKEY }}" > ~/.ssh/id_ed25519
chmod 0600 ~/.ssh/id_ed25519
echo "${{ vars.SERVER_NAME }} ${{ vars.SERVER_SSH_PUBKEY }}" >> ~/.ssh/known_hosts
chmod 0600 ~/.ssh/known_hosts
- name: push public and update symlink
run: | # scp erzeugt keine Zielordner, daher direkt alles via sftp erstellen und kopieren (symlink erstellt nur wenn noch nicht vorhanden)
echo "Copy files to ${{ matrix.builds.folder }}${{ steps.date.outputs.date }} and symlink to ${{ matrix.builds.folder }}"
sftp -i ~/.ssh/id_ed25519 -P 2222 ${{ vars.SERVER_SFTP_LOGIN }}@${{ vars.SERVER_NAME }} <<EOF
mkdir ${{ matrix.builds.folder }}${{ steps.date.outputs.date }}
cd ${{ matrix.builds.folder }}${{ steps.date.outputs.date }}
put -r ./public/.
cd ..
rm ${{ matrix.builds.folder }}
symlink ${{ matrix.builds.folder }}${{ steps.date.outputs.date }} ${{ matrix.builds.folder }}
EOF
- name: cleanup ssh
run: rm ~/.ssh/*
Einen Nachteil bei der Variante mit dem symlink gibt es aber schon und zwar stapeln sich nun die Freigaben im Zielverzeichnis. Dies kommt daher, dass das entfernen eines kompletten Ordners via sftp nicht möglich ist. Die einfache Lösung für dieses Problem wäre ein Shell-Script, dass die Ordner aufräumt, welches über einen Cron-Job gerufen wird.
Upload mit rsync
Hier wäre noch ein Beispiel mit rsync welches ich im Nachgang gefunden habe. Hierbei wird gezeigt, wie die SSH-Schlüssel ohne die Erstellung der ID verwendet werden können. Und der Publc-Key wird direkt vom Server ermittelt, was aus dem Aspekt der Sicherheit nicht so sinnvoll ist.
- name: Deploy
run: |
eval $(ssh-agent -s)
echo "${{ secrets.SERVER_SFTP_PRIVKEY }}" | ssh-add -
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan "${{ vars.SERVER_NAME }}" > ~/.ssh/known_hosts
rsync -atv --progress ./public/ ${{ vars.SERVER_SFTP_LOGIN }}@${{ vars.SERVER_NAME }}:${{ matrix.builds.folder }}