From bf5fc97e1acc35aaf3aeedfe380233cc20cb9cbe Mon Sep 17 00:00:00 2001
From: Michel Roegl-Brunner
<73236783+michelroegl-brunner@users.noreply.github.com>
Date: Tue, 11 Feb 2025 18:45:50 +0100
Subject: [PATCH] Add Workflow to test Scripts (#2269)
---
.github/workflows/script-test.yml | 139 ++++++++++
.../scripts/app-test/pr-alpine-install.func | 88 ++++++
.../workflows/scripts/app-test/pr-build.func | 259 ++++++++++++++++++
.../scripts/app-test/pr-create-lxc.sh | 158 +++++++++++
.../scripts/app-test/pr-install.func | 93 +++++++
5 files changed, 737 insertions(+)
create mode 100644 .github/workflows/script-test.yml
create mode 100644 .github/workflows/scripts/app-test/pr-alpine-install.func
create mode 100644 .github/workflows/scripts/app-test/pr-build.func
create mode 100644 .github/workflows/scripts/app-test/pr-create-lxc.sh
create mode 100644 .github/workflows/scripts/app-test/pr-install.func
diff --git a/.github/workflows/script-test.yml b/.github/workflows/script-test.yml
new file mode 100644
index 00000000..d3fc16ca
--- /dev/null
+++ b/.github/workflows/script-test.yml
@@ -0,0 +1,139 @@
+name: Run Scripts on PVE Node
+on:
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'install/*.sh'
+ - 'ct/*.sh'
+
+jobs:
+ run-install-script:
+ runs-on: pvenode
+ steps:
+ - name: Checkout PR branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ fetch-depth: 0
+ - name: Add Git safe directory
+ run: |
+ git config --global --add safe.directory /__w/ProxmoxVE/ProxmoxVE
+
+ - name: Set up GH_TOKEN
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ echo "GH_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV
+
+ - name: Get changed files in PR
+ id: changed-files
+ run: |
+ CHANGED_FILES=$(gh pr diff --name-only ${{ github.event.pull_request.number }})
+ CHANGED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
+ echo "Changed files: $CHANGED_FILES"
+ echo "SCRIPT=$CHANGED_FILES" >> $GITHUB_ENV
+
+ - name: Get scripts
+ id: check-install-script
+ run: |
+ ALL_FILES=()
+ ADDED_FILES=()
+ for FILE in ${{ env.SCRIPT }}; do
+ if [[ $FILE =~ ^install/.*-install\.sh$ ]] || [[ $FILE =~ ^ct/.*\.sh$ ]]; then
+ STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
+ if [[ ! " ${ADDED_FILES[@]} " =~ " $STRIPPED_NAME " ]]; then
+ ALL_FILES+=("$FILE")
+ ADDED_FILES+=("$STRIPPED_NAME") # Mark this base file as added (without the path)
+ fi
+ fi
+ done
+ ALL_FILES=$(echo "${ALL_FILES[@]}" | xargs)
+ echo "$ALL_FILES"
+ echo "ALL_FILES=$ALL_FILES" >> $GITHUB_ENV
+
+ - name: Run scripts
+ id: run-install
+ continue-on-error: true
+ run: |
+ set +e
+ #run for each files in /ct
+ for FILE in ${{ env.ALL_FILES }}; do
+ echo "Running: $FILE"
+ STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
+ if [[ $FILE =~ ^install/.*-install\.sh$ ]]; then
+ CT_SCRIPT="ct/$STRIPPED_NAME.sh"
+ if [[ ! -f $CT_SCRIPT ]]; then
+ echo "No CT script found for $STRIPPED_NAME"
+ exit 1
+ fi
+ echo "Found CT script for $STRIPPED_NAME"
+ chmod +x "$CT_SCRIPT"
+ RUNNING_FILE=$CT_SCRIPT
+ elif [[ $FILE =~ ^ct/.*\.sh$ ]]; then
+ INSTALL_SCRIPT="install/$STRIPPED_NAME-install.sh"
+ if [[ ! -f $INSTALL_SCRIPT ]]; then
+ echo "No install script found for $STRIPPED_NAME"
+ exit 1
+ fi
+ echo "Found install script for $STRIPPED_NAME"
+ chmod +x "$INSTALL_SCRIPT"
+ RUNNING_FILE=$FILE
+ fi
+ git checkout origin/main .github/workflows/scripts/app-test/pr-build.func
+ git checkout origin/main .github/workflows/scripts/app-test/pr-install.func
+ git checkout origin/main .github/workflows/scripts/app-test/pr-alpine-install.func
+ git checkout origin/main .github/workflows/scripts/app-test/pr-create-lxc.sh
+ sed -i 's|source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)|source .github/workflows/scripts/app-test/pr-build.func|g' "$RUNNING_FILE"
+ chmod +x $RUNNING_FILE
+ chmod +x .github/workflows/scripts/app-test/pr-create-lxc.sh
+ chmod +x .github/workflows/scripts/app-test/pr-install.func
+ chmod +x .github/workflows/scripts/app-test/pr-alpine-install.func
+ chmod +x .github/workflows/scripts/app-test/pr-build.func
+
+ ERROR_MSG=$(./$RUNNING_FILE 2>&1 > /dev/null)
+ echo "Finished running $FILE"
+ if [ -n "$ERROR_MSG" ]; then
+ echo "ERROR in $STRIPPED_NAME: $ERROR_MSG"
+ echo "$ERROR_MSG" > result_$STRIPPED_NAME.log
+ fi
+ done
+ set -e # Restore exit-on-error
+
+ - name: Cleanup PVE Node
+ run: |
+ containers=$(pct list | tail -n +2 | awk '{print $0 " " $4}' | awk '{print $1}')
+
+ for container_id in $containers; do
+ status=$(pct status $container_id | awk '{print $2}')
+ if [[ $status == "running" ]]; then
+ pct stop $container_id
+ pct destroy $container_id
+ fi
+ done
+
+ - name: Post error comments
+ run: |
+ ERROR="false"
+ SEARCH_LINE=".github/workflows/scripts/app-test/pr-build.func: line 113:"
+ for FILE in ${{ env.ALL_FILES }}; do
+ STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
+ if [[ ! -f result_$STRIPPED_NAME.log ]]; then
+ continue
+ fi
+ ERROR_MSG=$(cat result_$STRIPPED_NAME.log)
+
+ if [ -n "$ERROR_MSG" ]; then
+ CLEANED_ERROR_MSG=$(echo "$ERROR_MSG" | sed "s|$SEARCH_LINE.*||")
+ echo "Posting error message for $FILE"
+ echo ${CLEANED_ERROR_MSG}
+ gh pr comment ${{ github.event.pull_request.number }} \
+ --body ":warning: The script _**$FILE**_ failed with the following message:
${CLEANED_ERROR_MSG}
"
+
+ ERROR="true"
+ fi
+ done
+ echo "ERROR=$ERROR" >> $GITHUB_ENV
+ - name: Fail if error
+ if: ${{ env.ERROR }} == 'true'
+ run: exit 1
diff --git a/.github/workflows/scripts/app-test/pr-alpine-install.func b/.github/workflows/scripts/app-test/pr-alpine-install.func
new file mode 100644
index 00000000..39a6a82f
--- /dev/null
+++ b/.github/workflows/scripts/app-test/pr-alpine-install.func
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+color() {
+ return
+}
+catch_errors() {
+ set -Eeuo pipefail
+ trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+}
+
+
+error_handler() {
+ local line_number="$1"
+ local command="$2"
+ SCRIPT_NAME=$(basename "$0")
+ local error_message="$SCRIPT_NAME: Failure in line $line_number while executing command $command"
+ echo -e "\n$error_message"
+ exit 0
+}
+verb_ip6() {
+ STD=""
+ return
+}
+
+msg_info() {
+ local msg="$1"
+ echo -ne "${msg}\n"
+}
+
+msg_ok() {
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+msg_error() {
+
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+RETRY_NUM=10
+RETRY_EVERY=3
+i=$RETRY_NUM
+
+setting_up_container() {
+ while [ $i -gt 0 ]; do
+ if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
+ break
+ fi
+ echo 1>&2 -en "No Network! "
+ sleep $RETRY_EVERY
+ i=$((i - 1))
+ done
+
+ if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
+ echo 1>&2 -e "\n No Network After $RETRY_NUM Tries"
+ echo -e "Check Network Settings"
+ exit 1
+ fi
+ msg_ok "Set up Container OS"
+ msg_ok "Network Connected: $(hostname -i)"
+}
+
+network_check() {
+ RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
+ if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to $RESOLVEDIP"; fi
+ set -e
+}
+
+update_os() {
+ msg_info "Updating Container OS"
+ apk update
+ apk upgrade
+ msg_ok "Updated Container OS"
+}
+
+motd_ssh() {
+ return
+}
+
+customize() {
+ return
+}
+
+
diff --git a/.github/workflows/scripts/app-test/pr-build.func b/.github/workflows/scripts/app-test/pr-build.func
new file mode 100644
index 00000000..5706dbbb
--- /dev/null
+++ b/.github/workflows/scripts/app-test/pr-build.func
@@ -0,0 +1,259 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+variables() {
+ NSAPP=$(echo ${APP,,} | tr -d ' ')
+ var_install="${NSAPP}-install"
+
+}
+
+NEXTID=$(pvesh get /cluster/nextid)
+timezone=$(cat /etc/timezone)
+header_info(){
+ return
+}
+
+base_settings() {
+
+ CT_TYPE="1"
+ DISK_SIZE="4"
+ CORE_COUNT="1"
+ RAM_SIZE="1024"
+ VERBOSE="${1:-no}"
+ PW=""
+ CT_ID=$NEXTID
+ HN="Testing"
+ BRG="vmbr0"
+ NET="dhcp"
+ GATE=""
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ DISABLEIP6="no"
+ MTU=""
+ SD=""
+ NS=""
+ MAC=""
+ VLAN=""
+ SSH="no"
+ SSH_AUTHORIZED_KEY=""
+ TAGS="community-script;"
+
+
+ CT_TYPE=${var_unprivileged:-$CT_TYPE}
+ DISK_SIZE=${var_disk:-$DISK_SIZE}
+ CORE_COUNT=${var_cpu:-$CORE_COUNT}
+ RAM_SIZE=${var_ram:-$RAM_SIZE}
+ VERB=${var_verbose:-$VERBOSE}
+ TAGS="${TAGS}${var_tags:-}"
+
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+color() {
+ # Colors
+ YW=$(echo "\033[33m")
+ YWB=$(echo "\033[93m")
+ BL=$(echo "\033[36m")
+ RD=$(echo "\033[01;31m")
+ BGN=$(echo "\033[4;92m")
+ GN=$(echo "\033[1;92m")
+ DGN=$(echo "\033[32m")
+
+ # Formatting
+ CL=$(echo "\033[m")
+ UL=$(echo "\033[4m")
+ BOLD=$(echo "\033[1m")
+ BFR="\\r\\033[K"
+ HOLD=" "
+ TAB=" "
+
+ # Icons
+ CM="${TAB}✔️${TAB}${CL}"
+ CROSS="${TAB}✖️${TAB}${CL}"
+ INFO="${TAB}💡${TAB}${CL}"
+ OS="${TAB}🖥️${TAB}${CL}"
+ OSVERSION="${TAB}🌟${TAB}${CL}"
+ CONTAINERTYPE="${TAB}📦${TAB}${CL}"
+ DISKSIZE="${TAB}💾${TAB}${CL}"
+ CPUCORE="${TAB}🧠${TAB}${CL}"
+ RAMSIZE="${TAB}🛠️${TAB}${CL}"
+ SEARCH="${TAB}🔍${TAB}${CL}"
+ VERIFYPW="${TAB}🔐${TAB}${CL}"
+ CONTAINERID="${TAB}🆔${TAB}${CL}"
+ HOSTNAME="${TAB}🏠${TAB}${CL}"
+ BRIDGE="${TAB}🌉${TAB}${CL}"
+ NETWORK="${TAB}📡${TAB}${CL}"
+ GATEWAY="${TAB}🌐${TAB}${CL}"
+ DISABLEIPV6="${TAB}🚫${TAB}${CL}"
+ DEFAULT="${TAB}⚙️${TAB}${CL}"
+ MACADDRESS="${TAB}🔗${TAB}${CL}"
+ VLANTAG="${TAB}🏷️${TAB}${CL}"
+ ROOTSSH="${TAB}🔑${TAB}${CL}"
+ CREATING="${TAB}🚀${TAB}${CL}"
+ ADVANCED="${TAB}🧩${TAB}${CL}"
+}
+
+catch_errors() {
+ set -Eeuo pipefail
+ trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+}
+
+# This function handles errors
+error_handler() {
+ local line_number="$1"
+ local command="$2"
+ SCRIPT_NAME=$(basename "$0")
+ local error_message="$SCRIPT_NAME: Failure in line $line_number while executing command $command"
+ echo -e "\n$error_message"
+ exit "$error_message"
+}
+
+
+msg_info() {
+ local msg="$1"
+ echo -ne "${msg}\n"
+}
+
+msg_ok() {
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+msg_error() {
+
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+start(){
+ return
+}
+
+build_container() {
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+ TEMP_DIR=$(mktemp -d)
+ pushd $TEMP_DIR >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(cat /root/actions-runner/_work/ProxmoxVE/ProxmoxVE/.github/workflows/scripts/app-test/pr-alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(cat /root/actions-runner/_work/ProxmoxVE/ProxmoxVE/.github/workflows/scripts/app-test/pr-install.func)"
+ fi
+
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz=""
+ export DISABLEIPV6="$DISABLEIP6"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERB"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export tz="$timezone"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ -net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+ "
+ echo "Container ID: $CTID"
+
+
+ # This executes create_lxc.sh and creates the container and .conf file
+ bash /root/actions-runner/_work/ProxmoxVE/ProxmoxVE/.github/workflows/scripts/app-test/pr-create-lxc.sh
+
+ LXC_CONFIG=/etc/pve/lxc/${CTID}.conf
+ if [ "$CT_TYPE" == "0" ]; then
+ cat <>$LXC_CONFIG
+# USB passthrough
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ fi
+
+ if [ "$CT_TYPE" == "0" ]; then
+ if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" ]]; then
+ cat <>$LXC_CONFIG
+# VAAPI hardware transcoding
+lxc.cgroup2.devices.allow: c 226:0 rwm
+lxc.cgroup2.devices.allow: c 226:128 rwm
+lxc.cgroup2.devices.allow: c 29:0 rwm
+lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file
+lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir
+lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file
+EOF
+ fi
+ else
+ if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" ]]; then
+ if [[ -e "/dev/dri/renderD128" ]]; then
+ if [[ -e "/dev/dri/card0" ]]; then
+ cat <>$LXC_CONFIG
+# VAAPI hardware transcoding
+dev0: /dev/dri/card0,gid=44
+dev1: /dev/dri/renderD128,gid=104
+EOF
+ else
+ cat <>$LXC_CONFIG
+# VAAPI hardware transcoding
+dev0: /dev/dri/card1,gid=44
+dev1: /dev/dri/renderD128,gid=104
+EOF
+ fi
+ fi
+ fi
+ fi
+
+ # This starts the container and executes -install.sh
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+ msg_ok "Started LXC Container"
+
+ if [[ ! -f "/root/actions-runner/_work/ProxmoxVE/ProxmoxVE/install/$var_install.sh" ]]; then
+ msg_error "No install script found for $APP"
+ exit 1
+ fi
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash >/dev/null"
+ fi
+ lxc-attach -n "$CTID" -- bash -c "$(< /root/actions-runner/_work/ProxmoxVE/ProxmoxVE/install/$var_install.sh)"
+
+}
+
+description(){
+ return
+}
diff --git a/.github/workflows/scripts/app-test/pr-create-lxc.sh b/.github/workflows/scripts/app-test/pr-create-lxc.sh
new file mode 100644
index 00000000..3dd792b1
--- /dev/null
+++ b/.github/workflows/scripts/app-test/pr-create-lxc.sh
@@ -0,0 +1,158 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+color() {
+ return
+}
+catch_errors() {
+ set -Eeuo pipefail
+ trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+}
+
+
+error_handler() {
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ local error_message="Failure in line $line_number: exit code $exit_code: while executing command $command"
+ echo -e "\n$error_message"
+ exit 100
+}
+verb_ip6() {
+ return
+}
+
+msg_info() {
+ local msg="$1"
+ echo -ne "${msg}\n"
+}
+
+msg_ok() {
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+msg_error() {
+
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+
+VALIDCT=$(pvesm status -content rootdir | awk 'NR>1')
+if [ -z "$VALIDCT" ]; then
+ msg_error "Unable to detect a valid Container Storage location."
+ exit 1
+fi
+VALIDTMP=$(pvesm status -content vztmpl | awk 'NR>1')
+if [ -z "$VALIDTMP" ]; then
+ msg_error "Unable to detect a valid Template Storage location."
+ exit 1
+fi
+
+function select_storage() {
+ local CLASS=$1
+ local CONTENT
+ local CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ *) false || { msg_error "Invalid storage class."; exit 201; };;
+ esac
+
+ local -a MENU
+ while read -r line; do
+ local TAG=$(echo $line | awk '{print $1}')
+ local TYPE=$(echo $line | awk '{printf "%-10s", $2}')
+ local FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
+ local ITEM="Type: $TYPE Free: $FREE "
+ local OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ local MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ MENU+=("$TAG" "$ITEM" "OFF")
+ done < <(pvesm status -content $CONTENT | awk 'NR>1')
+
+ if [ $((${#MENU[@]}/3)) -eq 1 ]; then
+ printf ${MENU[0]}
+ else
+ msg_error "STORAGE ISSUES!"
+ exit 202
+ fi
+}
+
+
+
+[[ "${CTID:-}" ]] || { msg_error "You need to set 'CTID' variable."; exit 203; }
+[[ "${PCT_OSTYPE:-}" ]] || { msg_error "You need to set 'PCT_OSTYPE' variable."; exit 204; }
+
+[ "$CTID" -ge "100" ] || { msg_error "ID cannot be less than 100."; exit 205; }
+
+if pct status $CTID &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+fi
+
+
+TEMPLATE_STORAGE=$(select_storage template) || exit
+msg_ok "Using $TEMPLATE_STORAGE for Template Storage."
+
+
+CONTAINER_STORAGE=$(select_storage container) || exit
+msg_ok "Using $CONTAINER_STORAGE for Container Storage."
+
+msg_info "Updating LXC Template List"
+pveam update >/dev/null
+msg_ok "Updated LXC Template List"
+
+TEMPLATE_SEARCH=${PCT_OSTYPE}-${PCT_OSVERSION:-}
+mapfile -t TEMPLATES < <(pveam available -section system | sed -n "s/.*\($TEMPLATE_SEARCH.*\)/\1/p" | sort -t - -k 2 -V)
+[ ${#TEMPLATES[@]} -gt 0 ] || { msg_error "Unable to find a template when searching for '$TEMPLATE_SEARCH'."; exit 207; }
+TEMPLATE="${TEMPLATES[-1]}"
+
+TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+
+if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ msg_info "Downloading LXC Template"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null ||
+ { msg_error "A problem occurred while downloading the LXC template."; exit 208; }
+ msg_ok "Downloaded LXC Template"
+fi
+
+
+grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >> /etc/subuid
+grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >> /etc/subgid
+
+
+PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+echo "${PCT_OPTIONS[@]}"
+
+
+msg_info "Creating LXC Container"
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+
+ msg_ok "Template integrity check completed"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null ||
+ { msg_error "A problem occurred while re-downloading the LXC template."; exit 208; }
+
+ msg_ok "Re-downloaded LXC Template"
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
+ msg_error "A problem occurred while trying to create container after re-downloading template."
+ exit 200
+ fi
+ fi
+msg_ok "LXC Container $CTID was successfully created."
diff --git a/.github/workflows/scripts/app-test/pr-install.func b/.github/workflows/scripts/app-test/pr-install.func
new file mode 100644
index 00000000..60e202d5
--- /dev/null
+++ b/.github/workflows/scripts/app-test/pr-install.func
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+color() {
+ return
+}
+catch_errors() {
+ set -Eeuo pipefail
+ trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+}
+
+error_handler() {
+ local line_number="$1"
+ local command="$2"
+ SCRIPT_NAME=$(basename "$0")
+ local error_message="$SCRIPT_NAME: Failure in line $line_number while executing command $command"
+ echo -e "\n$error_message"
+ exit "$error_message"
+}
+verb_ip6() {
+ STD=""
+ return
+}
+
+msg_info() {
+ local msg="$1"
+ echo -ne "${msg}\n"
+}
+
+msg_ok() {
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+
+msg_error() {
+
+ local msg="$1"
+ echo -e "${msg}\n"
+}
+ RETRY_NUM=10
+ RETRY_EVERY=3
+setting_up_container() {
+ sed -i "/$LANG/ s/\(^# \)//" /etc/locale.gen
+ locale_line=$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print $1}' | head -n 1)
+ echo "LANG=${locale_line}" >/etc/default/locale
+ locale-gen >/dev/null
+ export LANG=${locale_line}
+ echo $tz >/etc/timezone
+ ln -sf /usr/share/zoneinfo/$tz /etc/localtime
+
+ for ((i = RETRY_NUM; i > 0; i--)); do
+ if [ "$(hostname -I)" != "" ]; then
+ break
+ fi
+ echo 1>&2 -en "No Network! "
+ sleep $RETRY_EVERY
+ done
+ if [ "$(hostname -I)" = "" ]; then
+ echo 1>&2 -e "\nNo Network After $RETRY_NUM Tries"
+ echo -e "Check Network Settings"
+ exit 101
+ fi
+ rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
+ systemctl disable -q --now systemd-networkd-wait-online.service
+ msg_ok "Set up Container OS"
+ msg_ok "Network Connected: $(hostname -I)"
+}
+
+network_check() {
+ RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
+ if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to $RESOLVEDIP"; fi
+ set -e
+}
+
+update_os() {
+ msg_info "Updating Container OS"
+ apt-get update
+ apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
+ rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
+ msg_ok "Updated Container OS"
+}
+
+motd_ssh() {
+ return
+}
+
+customize() {
+ return
+}
+
+