diff --git a/.github/workflows/push_json_to_pocketbase.yml b/.github/workflows/push_json_to_pocketbase.yml index 4dc7e21aa..accb1d588 100644 --- a/.github/workflows/push_json_to_pocketbase.yml +++ b/.github/workflows/push_json_to_pocketbase.yml @@ -9,7 +9,7 @@ on: jobs: push-json: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/ct/caddymanager.sh b/ct/caddymanager.sh index 75990330f..db1a225d1 100644 --- a/ct/caddymanager.sh +++ b/ct/caddymanager.sh @@ -29,10 +29,40 @@ function update_script() { exit fi - msg_info "Updating Debian LXC" - $STD apt update - $STD apt upgrade -y - msg_ok "Updated Debian LXC" + if check_for_gh_release "caddymanager" "caddymanager/caddymanager"; then + msg_info "Stopping Service" + systemctl stop caddymanager-backend + systemctl stop caddymanager-frontend + msg_ok "Stopped Service" + + msg_info "Backing up configuration" + cp /opt/caddymanager/caddymanager.env /opt/ + cp /opt/caddymanager/caddymanager.sqlite /opt/ + cp /opt/caddymanager/frontend/Caddyfile /opt/ + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "caddymanager" "caddymanager/caddymanager" "tarball" + + msg_info "Installing CaddyManager" + cd /opt/caddymanager/backend + $STD npm install + cd /opt/caddymanager/frontend + $STD npm install + $STD npm run build + msg_ok "Installed CaddyManager" + + msg_info "Restoring configuration" + mv /opt/caddymanager.env /opt/caddymanager/ + mv /opt/caddymanager.sqlite /opt/caddymanager/ + mv -f /opt/Caddyfile /opt/caddymanager/frontend/ + msg_ok "Restored configuration" + + msg_info "Starting Service" + systemctl start caddymanager-backend + systemctl start caddymanager-frontend + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi cleanup_lxc exit } diff --git a/ct/github-runner.sh b/ct/github-runner.sh new file mode 100644 index 000000000..61556ed2e --- /dev/null +++ b/ct/github-runner.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/actions/runner + +APP="GitHub-Runner" +var_tags="${var_tags:-ci}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-8}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" +var_nesting="${var_nesting:-1}" +var_keyctl="${var_keyctl:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -f /opt/actions-runner/run.sh ]]; then + msg_error "No ${APP} Installation Found!" + exit 1 + fi + + if check_for_gh_release "actions-runner" "actions/runner"; then + msg_info "Stopping Service" + systemctl stop actions-runner + msg_ok "Stopped Service" + + msg_info "Backing up runner configuration" + BACKUP_DIR="/opt/actions-runner.backup" + mkdir -p "$BACKUP_DIR" + [[ -f /opt/actions-runner/.runner ]] && cp -a /opt/actions-runner/.runner "$BACKUP_DIR/" + [[ -f /opt/actions-runner/.credentials ]] && cp -a /opt/actions-runner/.credentials "$BACKUP_DIR/" + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-x64-*.tar.gz" + + msg_info "Restoring runner configuration" + [[ -f "$BACKUP_DIR/.runner" ]] && cp -a "$BACKUP_DIR/.runner" /opt/actions-runner/ + [[ -f "$BACKUP_DIR/.credentials" ]] && cp -a "$BACKUP_DIR/.credentials" /opt/actions-runner/ + rm -rf "$BACKUP_DIR" + msg_ok "Restored configuration" + + msg_info "Starting Service" + systemctl start actions-runner + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} After first boot, run config.sh with your token and start the service.${CL}" diff --git a/frontend/public/json/github-runner.json b/frontend/public/json/github-runner.json new file mode 100644 index 000000000..723393fee --- /dev/null +++ b/frontend/public/json/github-runner.json @@ -0,0 +1,48 @@ +{ + "name": "GitHub Runner", + "slug": "github-runner", + "categories": [ + 20 + ], + "date_created": "2026-03-04", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": null, + "documentation": "https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners", + "config_path": "/opt/actions-runner", + "website": "https://github.com/actions/runner", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/github.webp", + "description": "GitHub Actions self-hosted runner executes workflows for your repository or organization, enabling CI/CD, builds, and deployments.", + "install_methods": [ + { + "type": "default", + "script": "ct/github-runner.sh", + "resources": { + "cpu": 2, + "ram": 2048, + "hdd": 8, + "os": "Debian", + "version": "12" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Get your token: repo Settings → Actions → Runners → New self-hosted runner → copy the token.", + "type": "info" + }, + { + "text": "Then run: cd /opt/actions-runner && sudo -u runner ./config.sh --url https://github.com/your-username/your-repo --token ", + "type": "info" + }, + { + "text": "Start the runner: systemctl start actions-runner", + "type": "info" + } + ] +} diff --git a/install/caddymanager-install.sh b/install/caddymanager-install.sh index ba71cb62a..f5d77f498 100644 --- a/install/caddymanager-install.sh +++ b/install/caddymanager-install.sh @@ -32,6 +32,7 @@ $STD npm run build cat </opt/caddymanager/caddymanager.env PORT=3000 +APP_NAME=Caddy Manager DB_ENGINE=sqlite SQLITE_DB_PATH=/opt/caddymanager/caddymanager.sqlite CORS_ORIGIN=${LOCAL_IP}:80 diff --git a/install/github-runner-install.sh b/install/github-runner-install.sh new file mode 100644 index 000000000..4d7533d1a --- /dev/null +++ b/install/github-runner-install.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Copyright (c) 2021-2026 community-scripts ORG +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://docs.github.com/en/actions/hosting-your-own-runners + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + + + +NODE_VERSION="22" setup_nodejs + +msg_info "Creating runner user (no sudo)" +if ! getent passwd runner >/dev/null 2>&1; then + useradd -m -s /bin/bash runner +fi +msg_ok "Runner user ready" + +fetch_and_deploy_gh_release "actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-x64-*.tar.gz" + +msg_info "Setting ownership for runner user" +chown -R runner:runner /opt/actions-runner +msg_ok "Ownership set" + +msg_info "Creating Service" +cat </etc/systemd/system/actions-runner.service +[Unit] +Description=GitHub Actions self-hosted runner +Documentation=https://docs.github.com/en/actions/hosting-your-own-runners +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=runner +WorkingDirectory=/opt/actions-runner +ExecStart=/opt/actions-runner/run.sh +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q actions-runner +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/misc/tools.func b/misc/tools.func index 84f5e053b..e2486f636 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -8121,3 +8121,422 @@ function fetch_and_deploy_from_url() { msg_ok "Successfully deployed archive to $directory" return 0 } + +function fetch_and_deploy_gl_release() { + local app="$1" + local repo="$2" + local mode="${3:-tarball}" + local version="${var_appversion:-${4:-latest}}" + local target="${5:-/opt/$app}" + local asset_pattern="${6:-}" + + if [[ -z "$app" ]]; then + app="${repo##*/}" + if [[ -z "$app" ]]; then + msg_error "fetch_and_deploy_gl_release requires app name or valid repo" + return 1 + fi + fi + + local app_lc=$(echo "${app,,}" | tr -d ' ') + local version_file="$HOME/.${app_lc}" + + local api_timeout="--connect-timeout 10 --max-time 60" + local download_timeout="--connect-timeout 15 --max-time 900" + + local current_version="" + [[ -f "$version_file" ]] && current_version=$(<"$version_file") + + ensure_dependencies jq + + local repo_encoded + repo_encoded=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$repo" 2>/dev/null \ + || echo "$repo" | sed 's|/|%2F|g') + + local api_base="https://gitlab.com/api/v4/projects/$repo_encoded/releases" + local api_url + if [[ "$version" != "latest" ]]; then + api_url="$api_base/$version" + else + api_url="$api_base?per_page=1&order_by=released_at&sort=desc" + fi + + local header=() + [[ -n "${GITLAB_TOKEN:-}" ]] && header=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN") + + local max_retries=3 retry_delay=2 attempt=1 success=false http_code + + while ((attempt <= max_retries)); do + http_code=$(curl $api_timeout -sSL -w "%{http_code}" -o /tmp/gl_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true + if [[ "$http_code" == "200" ]]; then + success=true + break + elif [[ "$http_code" == "429" ]]; then + if ((attempt < max_retries)); then + msg_warn "GitLab API rate limit hit, retrying in ${retry_delay}s... (attempt $attempt/$max_retries)" + sleep "$retry_delay" + retry_delay=$((retry_delay * 2)) + fi + else + sleep "$retry_delay" + fi + ((attempt++)) + done + + if ! $success; then + if [[ "$http_code" == "401" ]]; then + msg_error "GitLab API authentication failed (HTTP 401)." + if [[ -n "${GITLAB_TOKEN:-}" ]]; then + msg_error "Your GITLAB_TOKEN appears to be invalid or expired." + else + msg_error "The repository may require authentication. Try: export GITLAB_TOKEN=\"glpat-your_token\"" + fi + elif [[ "$http_code" == "404" ]]; then + msg_error "GitLab project or release not found (HTTP 404)." + msg_error "Ensure '$repo' is correct and the project is accessible." + elif [[ "$http_code" == "429" ]]; then + msg_error "GitLab API rate limit exceeded (HTTP 429)." + msg_error "To increase the limit, export a GitLab token before running the script:" + msg_error " export GITLAB_TOKEN=\"glpat-your_token_here\"" + elif [[ "$http_code" == "000" || -z "$http_code" ]]; then + msg_error "GitLab API connection failed (no response)." + msg_error "Check your network/DNS: curl -sSL https://gitlab.com/api/v4/version" + else + msg_error "Failed to fetch release metadata (HTTP $http_code)" + fi + return 1 + fi + + local json tag_name + json=$(/dev/null || uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" + + local assets url_match="" + assets=$(_gl_asset_urls "$json") + + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in + $asset_pattern) + url_match="$u" + break + ;; + esac + done + fi + + if [[ -z "$url_match" ]]; then + for u in $assets; do + if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then + url_match="$u" + break + fi + done + fi + + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + + if [[ -z "$url_match" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "binary" "$asset_pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + assets=$(_gl_asset_urls "$json") + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in $asset_pattern) + url_match="$u"; break ;; + esac + done + fi + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]] && url_match="$u" && break + done + fi + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + fi + fi + + if [[ -z "$url_match" ]]; then + msg_error "No suitable .deb asset found for $app" + rm -rf "$tmpdir" + return 1 + fi + + filename="${url_match##*/}" + curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$url_match" || { + msg_error "Download failed: $url_match" + rm -rf "$tmpdir" + return 1 + } + + chmod 644 "$tmpdir/$filename" + local dpkg_opts="" + [[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold" + [[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew" + DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || { + SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || { + msg_error "Both apt and dpkg installation failed" + rm -rf "$tmpdir" + return 1 + } + } + + ### Prebuild Mode ### + elif [[ "$mode" == "prebuild" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + if [[ -z "$asset_url" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "prebuild" "$pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in $pattern) + asset_url="$u"; break ;; + esac + done + fi + fi + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + local unpack_tmp + unpack_tmp=$(mktemp -d) + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + if [[ "$filename" == *.zip ]]; then + ensure_dependencies unzip + unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || { + msg_error "Failed to extract ZIP archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + elif [[ "$filename" == *.tar.* || "$filename" == *.tgz || "$filename" == *.txz ]]; then + tar --no-same-owner -xf "$tmpdir/$filename" -C "$unpack_tmp" || { + msg_error "Failed to extract TAR archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unsupported archive format: $filename" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + + local top_entries inner_dir + top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) + if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then + inner_dir="$top_entries" + shopt -s dotglob nullglob + if compgen -G "$inner_dir/*" >/dev/null; then + cp -r "$inner_dir"/* "$target/" || { + msg_error "Failed to copy contents from $inner_dir to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Inner directory is empty: $inner_dir" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + else + shopt -s dotglob nullglob + if compgen -G "$unpack_tmp/*" >/dev/null; then + cp -r "$unpack_tmp"/* "$target/" || { + msg_error "Failed to copy contents to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unpacked archive is empty" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + fi + + ### Singlefile Mode ### + elif [[ "$mode" == "singlefile" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + if [[ -z "$asset_url" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "singlefile" "$pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in $pattern) + asset_url="$u"; break ;; + esac + done + fi + fi + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + mkdir -p "$target" + + local use_filename="${USE_ORIGINAL_FILENAME:-false}" + local target_file="$app" + [[ "$use_filename" == "true" ]] && target_file="$filename" + + curl $download_timeout -fsSL "${header[@]}" -o "$target/$target_file" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then + chmod +x "$target/$target_file" + fi + + else + msg_error "Unknown mode: $mode" + rm -rf "$tmpdir" + return 1 + fi + + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" + rm -rf "$tmpdir" +} \ No newline at end of file