Compare commits

..

11 Commits

Author SHA1 Message Date
f1cdfecaaf Update github-release.yml 2025-03-01 10:32:33 +01:00
c738615bf2 Update github-release.yml 2025-03-01 10:30:57 +01:00
8af885a0f3 Update github-release.yml 2025-03-01 10:29:07 +01:00
62cdb5c0c2 add manuall execution of release wf 2025-03-01 10:27:57 +01:00
843c4f8c1e [gh]: Fix Release Creation if Changelog.md to long (#2752) 2025-03-01 10:26:12 +01:00
d82c9a24a7 Update CHANGELOG.md (#2745)
Some checks failed
Shellcheck / Shellcheck (push) Waiting to run
Create Changelog Pull Request / update-changelog-pull-request (push) Waiting to run
Build and Publish Docker Image / build (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-28 15:17:48 +01:00
b24860c97b Update all Action to new selfhosted Runner Cluster (#2739)
* Update Runner

* Update Workflows
2025-02-28 15:15:29 +01:00
e8e298581c Update CHANGELOG.md (#2743)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-28 15:00:02 +01:00
05769fdff1 Update Script Test Workflow (#2741)
* Update Workflow

* Update
2025-02-28 14:56:58 +01:00
5c5c628954 Update CHANGELOG.md (#2740)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-28 14:03:38 +01:00
10f4172349 Shell Format Workflow (#2400) 2025-02-28 13:59:54 +01:00
14 changed files with 410 additions and 30 deletions

View File

@ -0,0 +1,68 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy as build
ARG TARGETOS
ARG TARGETARCH
ARG DOCKER_VERSION=27.5.1
ARG BUILDX_VERSION=0.20.1
ARG RUNNER_ARCH="x64"
RUN apt update -y && apt install sudo curl unzip -y
WORKDIR /actions-runner
RUN RUNNER_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep "tag_name" | head -n 1 | awk '{print substr($2, 3, length($2)-4)}') \
&& curl -f -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
&& tar xzf ./runner.tar.gz \
&& rm runner.tar.gz
RUN RUNNER_CONTAINER_HOOKS_VERSION=$(curl -s https://api.github.com/repos/actions/runner-container-hooks/releases/latest | grep "tag_name" | head -n 1 | awk '{print substr($2, 3, length($2)-4)}') \
&& curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s \
&& rm runner-container-hooks.zip
RUN export RUNNER_ARCH=${TARGETARCH} \
&& if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
&& if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
&& curl -fLo docker.tgz https://download.docker.com/${TARGETOS}/static/stable/${DOCKER_ARCH}/docker-${DOCKER_VERSION}.tgz \
&& tar zxvf docker.tgz \
&& rm -rf docker.tgz \
&& mkdir -p /usr/local/lib/docker/cli-plugins \
&& curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy
ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
ENV ImageOS=ubuntu22
RUN apt update -y \
&& apt install -y --no-install-recommends sudo lsb-release gpg-agent software-properties-common curl jq unzip \
&& rm -rf /var/lib/apt/lists/*
RUN add-apt-repository ppa:git-core/ppa \
&& apt update -y \
&& apt install -y git \
&& rm -rf /var/lib/apt/lists/*
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& groupadd docker --gid 123 \
&& usermod -aG sudo runner \
&& usermod -aG docker runner \
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers
# Install own dependencies in final image
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& apt-get install -y gh jq git
WORKDIR /home/runner
COPY --chown=runner:docker --from=build /actions-runner .
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx
RUN install -o root -g root -m 755 docker/* /usr/bin/ && rm -rf docker
USER runner

View File

@ -10,7 +10,7 @@ on:
jobs:
update-app-files:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
permissions:
contents: write

View File

@ -7,7 +7,7 @@ on:
jobs:
autolabeler:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
permissions:
pull-requests: write
env:

View File

@ -7,7 +7,7 @@ on:
jobs:
update-changelog-pull-request:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
env:
CONFIG_PATH: .github/changelog-pr-config.json
BRANCH_NAME: github-action-update-changelog

View File

@ -6,7 +6,7 @@ on:
jobs:
close-discussion:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
steps:
- name: Checkout Repository

View File

@ -0,0 +1,37 @@
name: Build and Publish Docker Image
on:
push:
branches:
- main
paths:
- '.github/runner/docker/**'
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: ubuntu-latest #To ensure it always builds we use the github runner with all the right tooling
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
run: |
repo_name=${{ github.repository }} # Get repository name
repo_name_lower=$(echo $repo_name | tr '[:upper:]' '[:lower:]') # Convert to lowercase
docker build -t ghcr.io/$repo_name_lower/gh-runner-self:latest -f .github/runner/docker/gh-runner-self.dockerfile .
- name: Push Docker image to GHCR
run: |
repo_name=${{ github.repository }} # Get repository name
repo_name_lower=$(echo $repo_name | tr '[:upper:]' '[:lower:]') # Convert to lowercase
docker push ghcr.io/$repo_name_lower/gh-runner-self:latest

View File

@ -9,7 +9,7 @@ on:
jobs:
delete_branch:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
steps:
- name: Checkout the code
uses: actions/checkout@v3

View File

@ -27,7 +27,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
defaults:
run:
working-directory: frontend # Set default working directory for all run steps

View File

@ -2,24 +2,39 @@ name: Create new release
on:
schedule:
# Runs "At 00:01 every night" (UTC)
- cron: '1 0 * * *'
- cron: '1 0 * * *' # Runs nightly
workflow_dispatch:
jobs:
create-new-release:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Parse CHANGELOG.md for yesterday's entries and create a new release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
YESTERDAY=$(date -u --date="yesterday" +%Y-%m-%d)
YESTERDAY_CHANGELOG_NOTES=$(awk '/^## '"$YESTERDAY"'/ {f=1; next} f && /^## [0-9]{4}-[0-9]{2}-[0-9]{2}/ {f=0} f && !/^## / {print}' CHANGELOG.md)
if [ -n "$YESTERDAY_CHANGELOG_NOTES" ]; then
gh release create "$YESTERDAY" -t "$YESTERDAY" -n "$YESTERDAY_CHANGELOG_NOTES" --latest
awk '/^## '"$YESTERDAY"'/ {f=1; next} f && /^## [0-9]{4}-[0-9]{2}-[0-9]{2}/ {f=0} f && !/^## / {print}' CHANGELOG.md > changelog_tmp.md
if [ ! -s changelog_tmp.md ]; then
echo "No changes found for $YESTERDAY, skipping release."
exit 0
fi
CHANGELOG_SIZE=$(wc -c < changelog_tmp.md)
echo "Changelog size: $CHANGELOG_SIZE bytes"
# Crop to last 10,000 bytes if too large
if [ "$CHANGELOG_SIZE" -gt 10000 ]; then
echo "WARNING: Changelog too large, cropping to last 10,000 bytes..."
tail -c 10000 changelog_tmp.md > changelog_cropped.md
mv changelog_cropped.md changelog_tmp.md
fi
echo "Creating GitHub release for $YESTERDAY..."
gh release create "$YESTERDAY" -t "$YESTERDAY" -F changelog_tmp.md

View File

@ -140,34 +140,38 @@ jobs:
fi
done
- name: Post error comments
run: |
ERROR="false"
SEARCH_LINE=".github/workflows/scripts/app-test/pr-build.func: line 255"
SEARCH_LINE=".github/workflows/scripts/app-test/pr-build.func: line 255:"
# Get all existing comments on the PR
EXISTING_COMMENTS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json comments --jq '.comments[].body')
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 }} \
--repo ${{ github.repository }} \
--body ":warning: The script _**$FILE**_ failed with the following message: <br> <div><strong>${CLEANED_ERROR_MSG}</strong></div>"
ERROR="true"
COMMENT_BODY=":warning: The script _**$FILE**_ failed with the following message: <br> <div><strong>${CLEANED_ERROR_MSG}</strong></div>"
# Check if the comment already exists
if echo "$EXISTING_COMMENTS" | grep -qF "$COMMENT_BODY"; then
echo "Skipping duplicate comment for $FILE"
else
echo "Posting error message for $FILE"
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--body "$COMMENT_BODY"
ERROR="true"
fi
fi
done
echo "ERROR=$ERROR" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fail if error
if: ${{ env.ERROR == 'true' }}
run: exit 1

243
.github/workflows/script_format.yml vendored Normal file
View File

@ -0,0 +1,243 @@
name: Script Format Check
permissions:
pull-requests: write
on:
pull_request_target:
branches:
- main
paths:
- 'install/*.sh'
- 'ct/*.sh'
jobs:
run-install-script:
runs-on: pvenode
steps:
- name: Checkout PR branch (supports forks)
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
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
run: |
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
CHANGED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
echo "Changed files: $CHANGED_FILES"
echo "SCRIPT=$CHANGED_FILES" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check scripts
id: run-install
continue-on-error: true
run: |
for FILE in ${{ env.SCRIPT }}; do
STRIPPED_NAME=$(basename "$FILE" | sed 's/-install//' | sed 's/\.sh$//')
echo "Running Test for: $STRIPPED_NAME"
FILE_STRIPPED="${FILE##*/}"
LOG_FILE="result_$FILE_STRIPPED.log"
if [[ $FILE =~ ^ct/.*\.sh$ ]]; then
FIRST_LINE=$(sed -n '1p' "$FILE")
[[ "$FIRST_LINE" != "#!/usr/bin/env bash" ]] && echo "Line 1 was $FIRST_LINE | Should be: #!/usr/bin/env bash" >> "$LOG_FILE"
SECOND_LINE=$(sed -n '2p' "$FILE")
[[ "$SECOND_LINE" != "source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)" ]] &&
echo "Line 2 was $SECOND_LINE | Should be: source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)" >> "$LOG_FILE"
THIRD_LINE=$(sed -n '3p' "$FILE")
if ! [[ "$THIRD_LINE" =~ ^#\ Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ community-scripts\ ORG$ || "$THIRD_LINE" =~ ^Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ tteck$ ]]; then
echo "Line 3 was $THIRD_LINE | Should be: # Copyright (c) 2021-2025 community-scripts ORG" >> "$LOG_FILE"
fi
EXPECTED_AUTHOR="# Author:"
EXPECTED_LICENSE="# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE"
EXPECTED_SOURCE="# Source:"
EXPECTED_EMPTY=""
for i in {4..7}; do
LINE=$(sed -n "${i}p" "$FILE")
case $i in
4)
[[ $LINE == $EXPECTED_AUTHOR* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_AUTHOR" >> $LOG_FILE
;;
5)
[[ "$LINE" == "$EXPECTED_LICENSE" ]] || printf "Line %d was: '%s' | Should be: '%s'\n" "$i" "$LINE" "$EXPECTED_LICENSE" >> $LOG_FILE
;;
6)
[[ $LINE == $EXPECTED_SOURCE* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_SOURCE" >> $LOG_FILE
;;
7)
[[ -z $LINE ]] || printf "Line %d was: '%s' | Should be empty\n" "$i" "$LINE" >> $LOG_FILE
;;
esac
done
EXPECTED_PREFIXES=(
"APP="
"var_tags="
"var_cpu=" # Must be a number
"var_ram=" # Must be a number
"var_disk=" # Must be a number
"var_os=" # Must be debian, alpine, or ubuntu
"var_version="
"var_unprivileged=" # Must be 0 or 1
)
for i in {8..15}; do
LINE=$(sed -n "${i}p" "$FILE")
INDEX=$((i - 8))
case $INDEX in
2|3|4) # var_cpu, var_ram, var_disk (must be numbers)
if [[ "$LINE" =~ ^${EXPECTED_PREFIXES[$INDEX]}([0-9]+)$ ]]; then
continue # Valid
else
echo "Line $i was '$LINE' | Should be: '${EXPECTED_PREFIXES[$INDEX]}<NUMBER>'" >> "$LOG_FILE"
fi
;;
5) # var_os (must be debian, alpine, or ubuntu)
if [[ "$LINE" =~ ^var_os=(debian|alpine|ubuntu)$ ]]; then
continue # Valid
else
echo "Line $i was '$LINE' | Should be: 'var_os=[debian|alpine|ubuntu]'" >> "$LOG_FILE"
fi
;;
7) # var_unprivileged (must be 0 or 1)
if [[ "$LINE" =~ ^var_unprivileged=[01]$ ]]; then
continue # Valid
else
echo "Line $i was '$LINE' | Should be: 'var_unprivileged=[0|1]'" >> "$LOG_FILE"
fi
;;
*) # Other lines (must start with expected prefix)
if [[ "$LINE" == ${EXPECTED_PREFIXES[$INDEX]}* ]]; then
continue # Valid
else
echo "Line $i was '$LINE' | Should start with '${EXPECTED_PREFIXES[$INDEX]}'" >> "$LOG_FILE"
fi
;;
esac
done
for i in {16..20}; do
LINE=$(sed -n "${i}p" "$FILE")
EXPECTED=(
"header_info \"$APP\""
"variables"
"color"
"catch_errors"
"function update_script() {"
)
[[ "$LINE" != "${EXPECTED[$((i-16))]}" ]] && echo "Line $i was $LINE | Should be: ${EXPECTED[$((i-16))]}" >> "$LOG_FILE"
done
cat "$LOG_FILE"
elif [[ $FILE =~ ^install/.*-install\.sh$ ]]; then
FIRST_LINE=$(sed -n '1p' "$FILE")
[[ "$FIRST_LINE" != "#!/usr/bin/env bash" ]] && echo "Line 1 was $FIRST_LINE | Should be: #!/usr/bin/env bash" >> "$LOG_FILE"
SECOND_LINE=$(sed -n '2p' "$FILE")
[[ -n "$SECOND_LINE" ]] && echo "Line 2 should be empty" >> "$LOG_FILE"
THIRD_LINE=$(sed -n '3p' "$FILE")
if ! [[ "$THIRD_LINE" =~ ^#\ Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ community-scripts\ ORG$ || "$THIRD_LINE" =~ ^Copyright\ \(c\)\ [0-9]{4}-[0-9]{4}\ tteck$ ]]; then
echo "Line 3 was $THIRD_LINE | Should be: # Copyright (c) 2021-2025 community-scripts ORG" >> "$LOG_FILE"
fi
EXPECTED_AUTHOR="# Author:"
EXPECTED_LICENSE="# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE"
EXPECTED_SOURCE="# Source:"
EXPECTED_EMPTY=""
for i in {4..7}; do
LINE=$(sed -n "${i}p" "$FILE")
case $i in
4)
[[ $LINE == $EXPECTED_AUTHOR* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_AUTHOR" >> $LOG_FILE
;;
5)
[[ "$LINE" == "$EXPECTED_LICENSE" ]] || printf "Line %d was: '%s' | Should be: '%s'\n" "$i" "$LINE" "$EXPECTED_LICENSE" >> $LOG_FILE
;;
6)
[[ $LINE == $EXPECTED_SOURCE* ]] || printf "Line %d was: '%s' | Should start with: '%s'\n" "$i" "$LINE" "$EXPECTED_SOURCE" >> $LOG_FILE
;;
7)
[[ -z $LINE ]] || printf "Line %d was: '%s' | Should be empty\n" "$i" "$LINE" >> $LOG_FILE
;;
esac
done
[[ "$(sed -n '8p' "$FILE")" != 'source /dev/stdin <<< "$FUNCTIONS_FILE_PATH"' ]] && echo 'Line 8 should be: source /dev/stdin <<< "$FUNCTIONS_FILE_PATH"' >> "$LOG_FILE"
for i in {9..14}; do
LINE=$(sed -n "${i}p" "$FILE")
EXPECTED=(
"color"
"verb_ip6"
"catch_errors"
"setting_up_container"
"network_check"
"update_os"
)
[[ "$LINE" != "${EXPECTED[$((i-9))]}" ]] && echo "Line $i was $LINE | Should be: ${EXPECTED[$((i-9))]}" >> "$LOG_FILE"
done
[[ -n "$(sed -n '15p' "$FILE")" ]] && echo "Line 15 should be empty" >> "$LOG_FILE"
[[ "$(sed -n '16p' "$FILE")" != 'msg_info "Installing Dependencies"' ]] && echo 'Line 16 should be: msg_info "Installing Dependencies"' >> "$LOG_FILE"
LAST_3_LINES=$(tail -n 3 "$FILE")
[[ "$LAST_3_LINES" != *"$STD apt-get -y autoremove"* ]] && echo 'Third to last line should be: $STD apt-get -y autoremove' >> "$LOG_FILE"
[[ "$LAST_3_LINES" != *"$STD apt-get -y autoclean"* ]] && echo 'Second to last line should be: $STD apt-get -y clean' >> "$LOG_FILE"
[[ "$LAST_3_LINES" != *'msg_ok "Cleaned"'* ]] && echo 'Last line should be: msg_ok "Cleaned"' >> "$LOG_FILE"
cat "$LOG_FILE"
fi
done
- name: Post error comments
run: |
ERROR="false"
for FILE in ${{ env.SCRIPT }}; do
FILE_STRIPPED="${FILE##*/}"
LOG_FILE="result_$FILE_STRIPPED.log"
echo $LOG_FILE
if [[ ! -f $LOG_FILE ]]; then
continue
fi
ERROR_MSG=$(cat $LOG_FILE)
if [ -n "$ERROR_MSG" ]; then
echo "Posting error message for $FILE"
echo ${ERROR_MSG}
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--body ":warning: The script _**$FILE**_ has the following formatting errors: <br> <div><strong>${ERROR_MSG}</strong></div>"
ERROR="true"
fi
done
echo "ERROR=$ERROR" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fail if error
if: ${{ env.ERROR == 'true' }}
run: exit 1

View File

@ -10,7 +10,7 @@ on:
jobs:
update-app-files:
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
permissions:
contents: write

View File

@ -10,7 +10,7 @@ on:
jobs:
check-files:
name: Check changed files
runs-on: ubuntu-latest
runs-on: runner-cluster-htl-set
permissions:
pull-requests: write

View File

@ -17,6 +17,19 @@ All LXC instances created using this repository come pre-installed with Midnight
Do not break established syntax in this file, as it is automatically updated by a Github Workflow
## 2025-02-28
### 🧰 Maintenance
- #### ✨ New Features
- Shell Format Workflow [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#2400](https://github.com/community-scripts/ProxmoxVE/pull/2400))
- #### 📂 Github
- Update all Action to new selfhosted Runner Cluster [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#2739](https://github.com/community-scripts/ProxmoxVE/pull/2739))
- Update Script Test Workflow [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#2741](https://github.com/community-scripts/ProxmoxVE/pull/2741))
## 2025-02-27
### 🆕 New Scripts