From 046900c6b28f0602f712215a465d3d71bb7a6097 Mon Sep 17 00:00:00 2001 From: Marc Went Date: Fri, 30 Oct 2026 00:01:43 +0100 Subject: [PATCH 1/2] feat: add Initiative native server installer Install verified Debian server packages in an unprivileged LXC with PostgreSQL and an HTTPS proxy. Preserve configuration and take an offline database backup before updates. Accept ready-but-degraded responses with warnings while requiring all database checks. Reload installed units before service startup and cover installation, updates and operator recovery with regressions. Signed-off-by: Marc Went --- .github/workflows/bash-syntax.yml | 8 + .github/workflows/scripts/initiative-test.sh | 280 +++++++++++++++++++ ct/initiative.sh | 135 +++++++++ docs/guides/initiative.md | 240 ++++++++++++++++ install/initiative-install.sh | 157 +++++++++++ json/initiative.json | 45 +++ 6 files changed, 865 insertions(+) create mode 100644 .github/workflows/scripts/initiative-test.sh create mode 100644 ct/initiative.sh create mode 100644 docs/guides/initiative.md create mode 100644 install/initiative-install.sh create mode 100644 json/initiative.json diff --git a/.github/workflows/bash-syntax.yml b/.github/workflows/bash-syntax.yml index f6cefc2189..3af258664e 100644 --- a/.github/workflows/bash-syntax.yml +++ b/.github/workflows/bash-syntax.yml @@ -11,6 +11,8 @@ on: - ".github/workflows/scripts/fedora-image-test.sh" - ".github/workflows/scripts/unifi-vm-test.sh" - ".github/workflows/scripts/zima-release-test.sh" + - ".github/workflows/scripts/initiative-test.sh" + - "docs/guides/initiative.md" - "json/unifi-os-server-vm.json" push: branches: [main] @@ -23,6 +25,8 @@ on: - ".github/workflows/scripts/fedora-image-test.sh" - ".github/workflows/scripts/unifi-vm-test.sh" - ".github/workflows/scripts/zima-release-test.sh" + - ".github/workflows/scripts/initiative-test.sh" + - "docs/guides/initiative.md" - "json/unifi-os-server-vm.json" # The engine moved to community-scripts/core, which syntax-checks its own @@ -64,3 +68,7 @@ jobs: - name: ZimaOS release and ISO selection if: ${{ hashFiles('vm/zimaos-vm.sh') != '' }} run: bash .github/workflows/scripts/zima-release-test.sh + + - name: Initiative installer and update failure boundaries + if: ${{ hashFiles('ct/initiative.sh') != '' }} + run: bash .github/workflows/scripts/initiative-test.sh diff --git a/.github/workflows/scripts/initiative-test.sh b/.github/workflows/scripts/initiative-test.sh new file mode 100644 index 0000000000..3a3317ecb8 --- /dev/null +++ b/.github/workflows/scripts/initiative-test.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# Exercise installer failure boundaries without root, network access or a guest. +set -Eeuo pipefail + +scripts_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) +test_root=$(mktemp -d) +trap 'rm -rf "$test_root"' EXIT +export TASK_TEST_ROOT TEST_CASE TEST_MODE + +cat <<'MOCKS' >"$test_root/framework.sh" +color() { :; } +verb_ip6() { :; } +catch_errors() { set -Eeuo pipefail; } +setting_up_container() { :; } +network_check() { :; } +update_os() { :; } +header_info() { :; } +variables() { :; } +check_container_storage() { :; } +check_container_resources() { :; } +msg_info() { :; } +msg_ok() { :; } +msg_warn() { printf 'WARN %s\n' "$*"; } +msg_error() { printf '%s\n' "$*" >&2; } +motd_ssh() { :; } +customize() { :; } +cleanup_lxc() { :; } +description() { :; } +build_container() { :; } +get_os_info() { + case "$1" in + id) printf 'debian\n' ;; + version) [[ "$TEST_CASE" == wrong-os ]] && printf '12\n' || printf '13\n' ;; + esac +} +random_password() { printf 'TestOnlyGeneratedPassword01234567'; } +setup_postgresql() { printf 'postgresql\n' >>"$TASK_TEST_ROOT/operations"; } +setup_postgresql_db() { printf 'database\n' >>"$TASK_TEST_ROOT/operations"; } +get_latest_github_release() { printf 'v0.76.0\n'; } +check_for_gh_release() { + CHECK_UPDATE_RELEASE=v0.76.0 + [[ "$TEST_CASE" != no-update ]] +} +fetch_and_deploy_gh_release() { + [[ "$2" == beyonders-studio/initiative && "$3" == singlefile && "$4" == v0.76.0 ]] + [[ "${USE_ORIGINAL_FILENAME:-}" == true ]] + [[ "${FORCE_UPDATE:-}" == 1 ]] + printf 'fetch %s\n' "$6" >>"$TASK_TEST_ROOT/operations" + if [[ "$6" == initiative-server_0.76.0-\*_amd64.deb ]]; then + [[ "$TEST_CASE" != missing-asset ]] || return 252 + printf 'fixture package\n' >"$5/initiative-server_0.76.0-1_amd64.deb" + elif [[ "$6" == initiative-server-deb-amd64.sha256 ]]; then + (cd "$5" && sha256sum initiative-server_0.76.0-1_amd64.deb >initiative-server-deb-amd64.sha256) + if [[ "$TEST_CASE" == corrupt ]]; then + printf 'corruption\n' >>"$5/initiative-server_0.76.0-1_amd64.deb" + fi + else + return 65 + fi +} +dpkg() { printf 'amd64\n'; } +dpkg-deb() { + case "$3" in + Package) [[ "$TEST_CASE" == desktop-package ]] && printf 'initiative\n' || printf 'initiative-server\n' ;; + Architecture) [[ "$TEST_CASE" == wrong-arch ]] && printf 'arm64\n' || printf 'amd64\n' ;; + esac +} +apt() { + printf 'apt %s\n' "${*: -1}" >>"$TASK_TEST_ROOT/operations" + if [[ "${*: -1}" == *.deb ]]; then + touch "$TASK_TEST_ROOT/usr/bin/initiative-server" + chmod +x "$TASK_TEST_ROOT/usr/bin/initiative-server" + touch "$TASK_TEST_ROOT/unit-changed" + fi +} +chown() { :; } +systemctl() { + printf 'systemctl %s\n' "$*" >>"$TASK_TEST_ROOT/operations" + if [[ "$1" == daemon-reload ]]; then + rm -f "$TASK_TEST_ROOT/unit-changed" + elif [[ "$TEST_CASE" == changed-unit && "$*" == *initiative-server* && + ( "$1" == start || "$1" == enable ) && -e "$TASK_TEST_ROOT/unit-changed" ]]; then + return 1 + fi +} +pg_lsclusters() { + if [[ "$TEST_CASE" == online-cluster ]]; then + printf '17 main 5432 online postgres /var/lib/postgresql/17/main log\n' + else + printf '17 main 5432 down postgres /var/lib/postgresql/17/main log\n' + fi +} +create_backup() { + printf 'backup\n' >>"$TASK_TEST_ROOT/operations" + [[ "$TEST_CASE" != backup-failure ]] || return 1 + [[ -d "$BACKUP_DIR" ]] +} +create_self_signed_cert() { :; } +nginx_enable_site() { :; } +curl() { + [[ " $* " == *" --fail-with-body "* ]] || return 2 + [[ "$TEST_CASE" != unready ]] || return 22 + while (($#)); do + if [[ "$1" == --output ]]; then + case "$TEST_CASE" in + degraded) + printf '{"status":"degraded","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok","notify_bus":"error","storage":"ok","rate_limit_store":"ok"}}\n' >"$2" + ;; + partial-ready) + printf '{"status":"ok","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok","notify_bus":"error"}}\n' >"$2" + ;; + required-error) + printf '{"status":"degraded","checks":{"database":"error","database_system":"ok","database_provisioning":"ok","notify_bus":"ok"}}\n' >"$2" + ;; + missing-database) + printf '{"status":"ok","checks":{"notify_bus":"ok"}}\n' >"$2" + ;; + invalid-ready) printf 'unexpected response\n' >"$2" ;; + empty-ready) : >"$2" ;; + unavailable) + printf '{"status":"unavailable","checks":{"database":"error","database_system":"ok","database_provisioning":"ok"}}\n' >"$2" + return 22 + ;; + duplicate-ready) + printf '%s\n' '{"status":"ok","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok"}}' '{"status":"ok","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok"}}' >"$2" + ;; + *) + printf '{"status":"ok","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok","notify_bus":"ok","storage":"ok","rate_limit_store":"ok"}}\n' >"$2" + ;; + esac + return + fi + shift + done + return 1 +} +start() { [[ "$TEST_MODE" != update ]] || update_script; } +STD="" +LOCAL_IP=192.0.2.10 +IP=192.0.2.10 +CREATING="" GN="" CL="" INFO="" YW="" GATEWAY="" BGN="" +MOCKS + +run_case() { + TEST_CASE=$1 + TEST_MODE=$2 + TASK_TEST_ROOT="$test_root/$TEST_CASE-$TEST_MODE" + mkdir -p "$TASK_TEST_ROOT"/{opt,etc/initiative,etc/nginx/sites-available,var/lib/initiative,usr/bin,root,framework/core} + cp "$test_root/framework.sh" "$TASK_TEST_ROOT/framework/core/build.func" + touch "$TASK_TEST_ROOT/operations" + local script + if [[ "$TEST_MODE" == update ]]; then + script=ct/initiative.sh + printf 'retained configuration\n' >"$TASK_TEST_ROOT/etc/initiative/server.env" + printf '0.75.0\n' >"$TASK_TEST_ROOT/root/.initiative" + touch "$TASK_TEST_ROOT/usr/bin/initiative-server" + chmod +x "$TASK_TEST_ROOT/usr/bin/initiative-server" + else + script=install/initiative-install.sh + fi + sed \ + -e "s|/etc/initiative|$TASK_TEST_ROOT/etc/initiative|g" \ + -e "s|/etc/nginx|$TASK_TEST_ROOT/etc/nginx|g" \ + -e "s|/etc/postgresql|$TASK_TEST_ROOT/etc/postgresql|g" \ + -e "s|/var/lib/initiative|$TASK_TEST_ROOT/var/lib/initiative|g" \ + -e "s|/var/lib/postgresql|$TASK_TEST_ROOT/var/lib/postgresql|g" \ + -e "s|/usr/bin/initiative-server|$TASK_TEST_ROOT/usr/bin/initiative-server|g" \ + -e "s|/opt/|$TASK_TEST_ROOT/opt/|g" \ + -e "s|\${HOME}/.initiative|\${TASK_TEST_ROOT}/root/.initiative|g" \ + "$scripts_root/$script" >"$TASK_TEST_ROOT/script.sh" + local result=0 + FUNCTIONS_FILE_PATH="$(cat "$test_root/framework.sh")" \ + COMMUNITY_SCRIPTS_CORE_DIR="$TASK_TEST_ROOT/framework" \ + bash "$TASK_TEST_ROOT/script.sh" >"$TASK_TEST_ROOT/output" 2>&1 || result=$? + if [[ "$3" == pass && "$result" != 0 ]] || [[ "$3" == fail && "$result" == 0 ]]; then + cat "$TASK_TEST_ROOT/output" >&2 + printf 'FAIL %s %s (exit %s)\n' "$TEST_MODE" "$TEST_CASE" "$result" >&2 + exit 1 + fi +} + +assert_no_operation() { + if grep -q "$1" "$TASK_TEST_ROOT/operations"; then + printf 'Unexpected operation in %s: %s\n' "$TEST_CASE" "$1" >&2 + exit 1 + fi +} + +for scenario in corrupt desktop-package wrong-arch missing-asset wrong-os; do + run_case "$scenario" install fail + assert_no_operation '^apt .*\.deb$' + assert_no_operation '^systemctl .*initiative-server' + [[ ! -e "$TASK_TEST_ROOT/root/.initiative" ]] + printf 'PASS install rejects %s before package activation\n' "$scenario" +done + +for scenario in unready partial-ready required-error missing-database invalid-ready empty-ready unavailable duplicate-ready; do + run_case "$scenario" install fail + [[ ! -e "$TASK_TEST_ROOT/root/.initiative" ]] + printf 'PASS %s first startup does not record an installed version\n' "$scenario" +done + +run_case valid install pass +[[ "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.76.0 ]] +grep -q '^ENABLE_PUBLIC_REGISTRATION=false$' "$TASK_TEST_ROOT/etc/initiative/server.env" +grep -q '^INITIATIVE_HOST=127.0.0.1$' "$TASK_TEST_ROOT/etc/initiative/server.env" +printf 'PASS successful install records the version after readiness\n' + +for install_mode in install update; do + run_case degraded "$install_mode" pass + [[ "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.76.0 ]] + grep -q '^WARN .*notify_bus' "$TASK_TEST_ROOT/output" + printf 'PASS %s accepts ready-but-degraded responses with a dependency warning\n' "$install_mode" + + run_case changed-unit "$install_mode" pass + awk '/^apt .*\.deb$/ { installed = NR } /^systemctl daemon-reload$/ { reload = NR } /^systemctl (start|enable).*initiative-server/ { if (!installed || !reload || installed >= reload || reload >= NR) exit 1; started = 1 } END { if (!started) exit 1 }' "$TASK_TEST_ROOT/operations" + printf 'PASS %s reloads the installed unit before starting Initiative\n' "$install_mode" +done + +for scenario in corrupt backup-failure online-cluster; do + run_case "$scenario" update fail + assert_no_operation '^apt .*\.deb$' + [[ "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.75.0 ]] + printf 'PASS update rejects %s before package replacement\n' "$scenario" +done + +for scenario in unready partial-ready required-error missing-database invalid-ready empty-ready unavailable duplicate-ready; do + run_case "$scenario" update fail + [[ "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.75.0 ]] + printf 'PASS %s update retains the previous installed version\n' "$scenario" +done + +run_case no-update update pass +[[ ! -s "$TASK_TEST_ROOT/operations" ]] +printf 'PASS current installation performs no package or service operations\n' + +run_case valid update pass +[[ "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.76.0 ]] +[[ "$(cat "$TASK_TEST_ROOT/etc/initiative/server.env")" == 'retained configuration' ]] +awk '/^backup$/ { backup = NR } /^apt .*\.deb$/ { if (!backup || backup >= NR) exit 1; installed = 1 } END { if (!installed) exit 1 }' "$TASK_TEST_ROOT/operations" +printf 'PASS successful update backs up before replacing the package\n' + +# Run the operator's documented recovery, including a failed transfer with valid JSON. +for recovery_case in valid reload-failure transfer-failure database-failure; do + ( + TASK_TEST_ROOT="$test_root/recovery-$recovery_case" + TEST_CASE=$recovery_case + mkdir -p "$TASK_TEST_ROOT"/{root,usr/lib/initiative} + printf '0.77.1\n' >"$TASK_TEST_ROOT/usr/lib/initiative/VERSION" + printf '0.76.0\n' >"$TASK_TEST_ROOT/root/.initiative" + # shellcheck disable=SC2329 # Exported into the child Bash process below. + systemctl() { [[ "$TEST_CASE" != reload-failure ]]; } + # shellcheck disable=SC2329 # Exported into the child Bash process below. + curl() { + if [[ "$TEST_CASE" == database-failure ]]; then + printf '{"status":"unavailable","checks":{"database":"error","database_system":"ok","database_provisioning":"ok"}}\n' + else + printf '{"status":"ok","checks":{"database":"ok","database_system":"ok","database_provisioning":"ok"}}\n' + fi + [[ "$TEST_CASE" != transfer-failure ]] + } + export -f systemctl curl + awk ' + /^### Recover an upgrade/ { section = 1 } + section && /^```bash/ { capture = 1; next } + capture && /^```/ { exit } + capture { print } + ' "$scripts_root/docs/guides/initiative.md" | + sed -e "s|/root/.initiative|$TASK_TEST_ROOT/root/.initiative|g" \ + -e "s|/usr/lib/initiative|$TASK_TEST_ROOT/usr/lib/initiative|g" >"$TASK_TEST_ROOT/recovery.sh" + result=0 + bash "$TASK_TEST_ROOT/recovery.sh" || result=$? + if [[ "$TEST_CASE" == valid ]]; then + [[ "$result" == 0 && "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.77.1 ]] + else + [[ "$result" != 0 && "$(cat "$TASK_TEST_ROOT/root/.initiative")" == 0.76.0 ]] + fi + printf 'PASS documented recovery %s preserves its version-marker gates\n' "$TEST_CASE" + ) +done diff --git a/ct/initiative.sh b/ct/initiative.sh new file mode 100644 index 0000000000..16f586609c --- /dev/null +++ b/ct/initiative.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main" +_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func" +source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func") + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Marc Went (Dunky13) +# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE +# Source: https://github.com/beyonders-studio/initiative + +APP="Initiative" +var_tags="${var_tags:-productivity;tasks;collaboration}" +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_arm64="${var_arm64:-yes}" +var_unprivileged="${var_unprivileged:-1}" +var_testurl="${var_testurl:-https://github.com/community-scripts/DevScripts/issues/2483}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -f /etc/initiative/server.env || ! -x /usr/bin/initiative-server ]]; then + msg_error "No Initiative installation found!" + exit 1 + fi + if [[ "$(get_os_info id)" != "debian" || "$(get_os_info version)" != "13" ]]; then + msg_error "Initiative server packages require Debian 13 with Python 3.13." + exit 1 + fi + + if check_for_gh_release "initiative" "beyonders-studio/initiative" "${var_appversion:-}"; then + release="$CHECK_UPDATE_RELEASE" + if [[ ! "$release" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + msg_error "Unsupported Initiative release tag: $release" + exit 1 + fi + arch=$(dpkg --print-architecture) + release_dir=$(mktemp -d /opt/initiative-release.XXXXXX) + FORCE_UPDATE=1 USE_ORIGINAL_FILENAME=true fetch_and_deploy_gh_release \ + "initiative-package" "beyonders-studio/initiative" "singlefile" "$release" "$release_dir" \ + "initiative-server_${release#v}-*_${arch}.deb" + FORCE_UPDATE=1 USE_ORIGINAL_FILENAME=true fetch_and_deploy_gh_release \ + "initiative-checksums" "beyonders-studio/initiative" "singlefile" "$release" "$release_dir" \ + "initiative-server-deb-${arch}.sha256" + + msg_info "Verifying Initiative Server Package" + (cd "$release_dir" && sha256sum --check --strict "initiative-server-deb-${arch}.sha256") + packages=("$release_dir"/initiative-server_"${release#v}"-*_"${arch}".deb) + if [[ "${#packages[@]}" != 1 || ! -f "${packages[0]}" ]] || + [[ "$(dpkg-deb --field "${packages[0]}" Package)" != "initiative-server" || + "$(dpkg-deb --field "${packages[0]}" Architecture)" != "$arch" ]]; then + msg_error "The Initiative release does not contain one matching native server package." + exit 1 + fi + msg_ok "Verified Initiative Server Package" + + msg_info "Stopping Initiative and PostgreSQL for Backup" + systemctl stop initiative-server postgresql@17-main postgresql + if ! pg_lsclusters --no-header | awk '$4 != "down" { exit 1 }'; then + msg_error "PostgreSQL is still running. Initiative was stopped; no package was replaced or database copied." + exit 1 + fi + msg_ok "Stopped Initiative and PostgreSQL for Backup" + + # An offline cluster copy includes the roles required by Initiative's RLS. + backup_dir=$(mktemp -d /opt/initiative-backup.XXXXXX) + BACKUP_DIR="$backup_dir" create_backup /etc/initiative /var/lib/initiative /etc/postgresql /var/lib/postgresql + + msg_info "Installing Initiative Server Update" + systemctl start postgresql postgresql@17-main + DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y \ + -o Dpkg::Options::=--force-confold "${packages[0]}" + # SYSTEMD_OFFLINE also skips the package hook's unit reload. + systemctl daemon-reload + systemctl start initiative-server + msg_ok "Installed Initiative Server Update" + + msg_info "Checking Initiative Readiness" + if ! $STD curl --fail-with-body --silent --retry 90 --retry-delay 2 \ + --retry-connrefused --retry-all-errors --retry-max-time 180 --max-time 5 \ + --output "$release_dir/ready.json" http://127.0.0.1:8173/api/v1/readyz; then + msg_error "Initiative readiness request failed. Inspect $release_dir/ready.json and journalctl -u initiative-server." + exit 1 + fi + if ! readiness_status=$(jq -ers ' + select(length == 1) | .[0] + | select(type == "object") + | select(.checks | type == "object") + | select( + (.status == "ok" or .status == "degraded") + and .checks.database == "ok" + and .checks.database_system == "ok" + and .checks.database_provisioning == "ok" + and (.checks | all(. == "ok" or . == "error")) + and (if .status == "ok" then (.checks | all(. == "ok")) + else (.checks | any(. == "error")) end) + ) + | .status + ' "$release_dir/ready.json"); then + msg_error "Initiative returned an invalid or unavailable readiness response. Inspect $release_dir/ready.json." + exit 1 + fi + degraded_checks=$(jq -r '.checks | to_entries | map(select(.value != "ok") | .key) | join(", ")' "$release_dir/ready.json") + # The release helpers track downloads; record installation only after startup succeeds. + cat <"${HOME}/.initiative" +${release#v} +EOF + rm -rf "$release_dir" + msg_ok "Initiative Is Ready" + if [[ "$readiness_status" == "degraded" ]]; then + msg_warn "Initiative is serving with degraded dependencies: $degraded_checks. Inspect journalctl -u initiative-server." + fi + msg_ok "Updated Initiative successfully. Pre-update backup: $backup_dir" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}Initiative setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW}Accept the self-signed certificate, then create the first owner account immediately:${CL}" +echo -e "${GATEWAY}${BGN}https://${IP}${CL}" diff --git a/docs/guides/initiative.md b/docs/guides/initiative.md new file mode 100644 index 0000000000..7173706a0e --- /dev/null +++ b/docs/guides/initiative.md @@ -0,0 +1,240 @@ +# Test Initiative on Proxmox + +This test installer is proposed for +[DevScripts](https://github.com/community-scripts/DevScripts), the required +testing stage before a new script can reach ProxmoxVE. It is not an accepted +Community Script. Initiative has 191 stars as of 2026-10-30, below DevScripts' +600-star requirement and the ProxmoxVE script-request form's 1,000-star +requirement. Consideration requires a maintainer exception; neither threshold +is claimed to be met. + +Record test results in the +[DevScripts testing issue](https://github.com/community-scripts/DevScripts/issues/2483). +The [ProxmoxVE discussion](https://github.com/community-scripts/ProxmoxVE/discussions/18319) +requests the adoption exception; the +[personal acceptance issue](https://github.com/Dunky13/ProxmoxVED/issues/1) +preserves the earlier testing history. + +## Create the container + +Run these commands as root in the **Proxmox host shell**, not in an existing +container: + +```bash +export COMMUNITY_SCRIPTS_URL=https://raw.githubusercontent.com/Dunky13/ProxmoxVED/refs/heads/feat/initiative +dev_mode=keep bash -c "$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/ct/initiative.sh")" +``` + +Keep the `export`: the engine needs it to find the matching installer in the +fork, and the container's `update` command remembers this source. Merely +downloading `ct/initiative.sh` from the fork does not select the fork's +`install/initiative-install.sh`. + +The standard Community Scripts wizard creates an **unprivileged Debian 13 LXC** +with **2 CPU cores, 2048 MiB RAM and an 8 GiB disk**. Choose its bridge, storage, +container ID and network settings in the wizard. These are initial test defaults, +not measured capacity guarantees. Leave the OS on Debian 13: the server package +requires its Python 3.13 ABI. The `keep` flag preserves a failed container for +diagnosis. Do not add the `trace` flag when collecting logs containing secrets. + +Open **`https://CONTAINER-IP`**, accept the self-signed certificate and create +your first account immediately. That account becomes the owner. Subsequent +public registration is disabled; there is no default username or password. +Keep the new instance on your private network while completing this step. + +## What it installs + +- The official `initiative-server` Debian release package, after checking its + matching architecture-specific SHA-256 manifest. It does not install the + Electron desktop client or rebuild the Python/browser dependencies. +- PostgreSQL 17 from Debian's repository, with a generated database password. +- The package's `initiative-server.service`, running as the package's + unprivileged `initiative` account. +- Nginx on HTTPS port 443, with HTTP port 80 redirected to HTTPS and WebSocket + proxying. Initiative listens on loopback port 8173. HTTPS is needed by browser + crypto features even when accessing the application by LAN address. + +The installer uses Community Scripts' setup, release, certificate and nginx +helpers. The native package already contains its Python environment, so there +is no `setup_uv`, pip or Node.js install. The database owner has the privileges +needed for initial RLS-role provisioning and the LEAKPROOF search operator. +Initiative disposes of that bootstrap connection before serving requests and +uses its separate application roles for runtime access. + +## Configuration and updates + +Configuration and generated secrets live in `/etc/initiative/server.env`, owned +by `root:initiative`, mode `0640`. Uploads live in +`/var/lib/initiative/uploads`. Preserve `SECRET_KEY`: it also protects encrypted +data. The installer refuses to overwrite an existing configuration. + +After changing the IP or using a domain, update `APP_URL` in the configuration +and run: + +```bash +systemctl restart initiative-server +``` + +Use a trusted HTTPS reverse proxy and certificate for a public domain. The +self-signed certificate is created for the container's original address. + +Take a Proxmox backup before upgrading. Then run **inside the container**: + +```bash +update +``` + +The updater checks for a newer release, downloads and verifies it before stopping +anything, then stops Initiative and PostgreSQL. It copies the local configuration, +uploads, PostgreSQL configuration and **stopped database cluster, including +roles**, into a private `/opt/initiative-backup.*` directory. It starts PostgreSQL, +installs the verified package with existing configuration preserved, and explicitly +reloads the systemd unit before starting Initiative. Readiness must pass before the +installed-version record moves forward. Backup directories remain for manual +recovery and consume disk space. + +Initiative can be ready while reporting `"status":"degraded"`: the three +database checks must be `"ok"`, while a failed optional dependency produces a +warning. The installer and updater accept that documented HTTP-200 state and name +the affected dependencies. HTTP failures, failed or missing required database +checks, inconsistent status values and malformed JSON still fail without moving +the installed-version record. Startup retries stay quiet; a final failure keeps +the response in the printed release directory and points to the service journal. + +This backup covers the installer's default local paths. If you move storage or +use an external database, arrange matching backups separately. A failed update +can leave services stopped. Inspect its error and backup before retrying. Do not +start an older package against a migrated database: restore a matching backup +or the whole Proxmox container instead. Do not blindly rerun the installer. + +## Check the result + +Inside the new container: + +```bash +systemctl is-active initiative-server nginx postgresql +curl --fail http://127.0.0.1:8173/api/v1/readyz +curl --insecure --fail https://127.0.0.1/api/v1/readyz +``` + +Expected: all three services are active and both readiness responses report +`"status":"ok"`, or `"status":"degraded"` with all three database checks `"ok"`. +Investigate the dependencies named `"error"` in a degraded response; they are +warnings rather than a failed installation. `--insecure` is only used here to +probe the local self-signed HTTPS endpoint. + +For a failed start: + +```bash +systemctl status initiative-server nginx postgresql --no-pager +journalctl -u initiative-server -n 100 --no-pager +``` + +Do not paste `server.env`, database passwords or account credentials into an +issue or chat. + +### Recover an upgrade rejected by the old readiness check + +The old script required every check to be `"ok"`, so it could report failure +after the package upgrade succeeded. For the reported 0.76.0 to 0.77.1 update, +the saved response supplied by the operator confirmed `"status":"degraded"` +with only `notify_bus` reporting `"error"`. All three database checks, storage +and the rate-limit store were `"ok"`. This is the exact response already covered +by the degraded-readiness regression fixture. A subsequent independent HTTPS +check confirmed v0.77.1 with every readiness check, including `notify_bus`, `"ok"`. + +If the app is running normally, these commands **inside the container** reload +the package unit and reconcile the version marker without reinstalling, +restarting the server or creating another backup: + +```bash +( + set -o pipefail + systemctl daemon-reload && + curl --fail --silent --show-error http://127.0.0.1:8173/api/v1/readyz | + jq -e '.status == "ok" and .checks.database == "ok" and .checks.database_system == "ok" and .checks.database_provisioning == "ok"' >/dev/null && + cat /usr/lib/initiative/VERSION > /root/.initiative +) +``` + +The marker is changed only after a fully healthy response; if that check fails, +inspect readiness and the journal before doing anything else. Keep the existing +pre-update backup until you have checked your data. The updated fork script is +used by subsequent `update` runs automatically. + +The operator has already reported a real amd64 Proxmox installation and a +0.76.0 to 0.77.1 package upgrade. Complete these remaining acceptance checks +with the corrected updater: + +1. Retest creation of an unprivileged Debian 13 container through the wizard + with the current branch. +2. Open HTTPS from your browser and create the first owner account. +3. Create a community, initiative, project and task; upload a file and exercise + live document editing. Confirm a second public registration is rejected. +4. Reboot the container and confirm the app, task and upload remain available. +5. Run `update`. A current installation should leave services and data alone. + When another server release is available, verify the upgrade and backup, + then recheck your task, upload and login. + +## Validation and handoff + +Local validation commands, from the fork checkout: + +```bash +bash -n ct/initiative.sh install/initiative-install.sh .github/workflows/scripts/initiative-test.sh +shellcheck -e SC1090,SC1091,SC2154 ct/initiative.sh install/initiative-install.sh .github/workflows/scripts/initiative-test.sh +bash .github/workflows/scripts/initiative-test.sh +jq -e . json/initiative.json +git diff --check +``` + +The ShellCheck exclusions cover dynamic framework imports and variables supplied +by Community Scripts. All 35 regression scenarios passed. The harness runs +without root or network access; it verifies checksum rejection, server-versus- +desktop package selection, the architecture and OS guards, missing assets, +unavailable or inconsistent readiness, valid degraded responses with named +warnings, failed backups, a still-running database, unit reload before start, +current-version no-op behavior, successful install/update ordering and the +documented recovery command's failure gates. The Bash Syntax Check workflow +runs this harness on pull requests and pushes to `main`. + +A disposable **Debian 13 arm64 VM** also passed these checks using Initiative +v0.76.0 and the real shared core helpers: + +- Release asset download and SHA-256 verification, native package installation, + PostgreSQL bootstrap, nginx HTTPS and systemd service activation as `initiative`. +- First-owner registration with public registration disabled, login, rejection of + subsequent public registration, community/initiative/project/task creation, + SPA delivery and the signed native update manifest. +- Reapplying the same release through the updater after setting an older version + marker: offline database-cluster backup, correct backup permissions, restart, + unchanged configuration and upload checksums, and preserved login/task data. +- A second update on the current version performed no service restart. + +The guest harness supplied the framework's environment and replaced the host +wizard, container provisioning, OS-update, resource-check and MOTD/cleanup steps. +Application installation and updater commands used real services and packages. +The cached-release skip found by this test was corrected by forcing downloads +into each fresh staging directory. The database shutdown check also required +stopping Debian's `postgresql@17-main` cluster unit explicitly. + +Native validation used core commit +`1ed248530539400a584892d9930b97cd9b823b92`, against DevScripts base +`2484a382cafe659ff63ffcf656a4e75f79975c02`. Development logs are local evidence, +not hosted CI artifacts. These guest checks do not establish arm64 LXC support +or verify the Proxmox wizard. Separately, the operator's amd64 Proxmox run +installed v0.76.0 and upgraded the official package to v0.77.1; subsequent HTTPS +checks confirmed that version and healthy readiness. The corrected full updater +has not been rerun on that container. Browser interactions, reboot persistence, +and preservation of real user data across the upgrade remain unverified. + +## Sources + +- [Initiative v0.76.0 release and server assets](https://github.com/beyonders-studio/initiative/releases/tag/v0.76.0) +- [Native package instructions](https://github.com/beyonders-studio/initiative/blob/v0.76.0/docs/en/running-a-server/native-packages.md) +- [Package lifecycle and service](https://github.com/beyonders-studio/initiative/tree/v0.76.0/packaging/server) +- [Database bootstrap implementation](https://github.com/beyonders-studio/initiative/blob/v0.76.0/backend/app/db/bootstrap.py) +- [First-owner registration behavior](https://github.com/beyonders-studio/initiative/blob/v0.76.0/backend/app/api/v1/platform_endpoints/auth.py) +- [DevScripts contribution and AI instructions](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) +- [Fork and branch source selection](https://github.com/community-scripts/DevScripts/blob/main/docs/guides/source-origin.md) +- [Shared core engine](https://github.com/community-scripts/core) diff --git a/install/initiative-install.sh b/install/initiative-install.sh new file mode 100644 index 0000000000..848f73d361 --- /dev/null +++ b/install/initiative-install.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Marc Went (Dunky13) +# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE +# Source: https://github.com/beyonders-studio/initiative + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +if [[ "$(get_os_info id)" != "debian" || "$(get_os_info version)" != "13" ]]; then + msg_error "Initiative server packages require Debian 13 with Python 3.13." + exit 1 +fi +if [[ -e /etc/initiative/server.env ]]; then + msg_error "Initiative is already configured. Run update instead of reinstalling." + exit 1 +fi + +msg_info "Installing Initiative Dependencies" +$STD apt install -y nginx +msg_ok "Installed Initiative Dependencies" + +PG_VERSION="17" USE_PGDG_REPO=false setup_postgresql +# Keep generated credentials out of shell traces and supplementary credentials files. +set +x +umask 0077 +PG_DB_PASS=$(random_password 32) +# Startup uses the owner only to provision RLS roles and the LEAKPROOF search operator. +PG_DB_NAME="initiative" PG_DB_USER="initiative" PG_DB_GRANT_SUPERUSER=true PG_DB_CREDS_FILE=/dev/null setup_postgresql_db + +release="${var_appversion:-$(get_latest_github_release "beyonders-studio/initiative" false)}" +if [[ ! "$release" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + msg_error "Unsupported Initiative release tag: $release" + exit 1 +fi +arch=$(dpkg --print-architecture) +release_dir=$(mktemp -d /opt/initiative-release.XXXXXX) +FORCE_UPDATE=1 USE_ORIGINAL_FILENAME=true fetch_and_deploy_gh_release \ + "initiative-package" "beyonders-studio/initiative" "singlefile" "$release" "$release_dir" \ + "initiative-server_${release#v}-*_${arch}.deb" +FORCE_UPDATE=1 USE_ORIGINAL_FILENAME=true fetch_and_deploy_gh_release \ + "initiative-checksums" "beyonders-studio/initiative" "singlefile" "$release" "$release_dir" \ + "initiative-server-deb-${arch}.sha256" + +msg_info "Verifying Initiative Server Package" +(cd "$release_dir" && sha256sum --check --strict "initiative-server-deb-${arch}.sha256") +packages=("$release_dir"/initiative-server_"${release#v}"-*_"${arch}".deb) +if [[ "${#packages[@]}" != 1 || ! -f "${packages[0]}" ]] || + [[ "$(dpkg-deb --field "${packages[0]}" Package)" != "initiative-server" || + "$(dpkg-deb --field "${packages[0]}" Architecture)" != "$arch" ]]; then + msg_error "The Initiative release does not contain one matching native server package." + exit 1 +fi +DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y "${packages[0]}" +msg_ok "Installed Verified Initiative Server Package" + +msg_info "Configuring Initiative" +cat </etc/initiative/server.env +SECRET_KEY=$(openssl rand -hex 32) +DATABASE_URL=postgresql+asyncpg://initiative:${PG_DB_PASS}@127.0.0.1:5432/initiative +APP_URL=https://${LOCAL_IP} +INITIATIVE_HOST=127.0.0.1 +INITIATIVE_PORT=8173 +BEHIND_PROXY=true +FORWARDED_ALLOW_IPS=127.0.0.1,::1 +UPLOADS_DIR=/var/lib/initiative/uploads +ENABLE_PUBLIC_REGISTRATION=false +EOF +chown root:initiative /etc/initiative/server.env +chmod 0640 /etc/initiative/server.env +unset PG_DB_PASS +msg_ok "Configured Initiative" + +# Web Crypto and passkeys require a secure browser context on LAN addresses too. +create_self_signed_cert "initiative" +msg_info "Configuring Initiative HTTPS Proxy" +cat <<'EOF' >/etc/nginx/sites-available/initiative +server { + listen 80; + listen [::]:80; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name _; + ssl_certificate /etc/ssl/initiative/initiative.crt; + ssl_certificate_key /etc/ssl/initiative/initiative.key; + client_max_body_size 100m; + + location / { + proxy_pass http://127.0.0.1:8173; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_buffering off; + } +} +EOF +nginx_enable_site "initiative" +# SYSTEMD_OFFLINE also skips the package hook's unit reload. +systemctl daemon-reload +systemctl enable -q --now nginx initiative-server +msg_ok "Configured Initiative HTTPS Proxy" + +msg_info "Checking Initiative Readiness" +if ! $STD curl --fail-with-body --silent --retry 90 --retry-delay 2 \ + --retry-connrefused --retry-all-errors --retry-max-time 180 --max-time 5 \ + --output "$release_dir/ready.json" http://127.0.0.1:8173/api/v1/readyz; then + msg_error "Initiative readiness request failed. Inspect $release_dir/ready.json and journalctl -u initiative-server." + exit 1 +fi +if ! readiness_status=$(jq -ers ' + select(length == 1) | .[0] + | select(type == "object") + | select(.checks | type == "object") + | select( + (.status == "ok" or .status == "degraded") + and .checks.database == "ok" + and .checks.database_system == "ok" + and .checks.database_provisioning == "ok" + and (.checks | all(. == "ok" or . == "error")) + and (if .status == "ok" then (.checks | all(. == "ok")) + else (.checks | any(. == "error")) end) + ) + | .status +' "$release_dir/ready.json"); then + msg_error "Initiative returned an invalid or unavailable readiness response. Inspect $release_dir/ready.json." + exit 1 +fi +degraded_checks=$(jq -r '.checks | to_entries | map(select(.value != "ok") | .key) | join(", ")' "$release_dir/ready.json") +cat <"${HOME}/.initiative" +${release#v} +EOF +rm -rf "$release_dir" +msg_ok "Initiative Is Ready" +if [[ "$readiness_status" == "degraded" ]]; then + msg_warn "Initiative is serving with degraded dependencies: $degraded_checks. Inspect journalctl -u initiative-server." +fi + +motd_ssh +customize +cleanup_lxc diff --git a/json/initiative.json b/json/initiative.json new file mode 100644 index 0000000000..64158b5e90 --- /dev/null +++ b/json/initiative.json @@ -0,0 +1,45 @@ +{ + "name": "Initiative", + "slug": "initiative", + "categories": [25, 12], + "date_created": "2026-10-30", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 443, + "documentation": "https://beyonders-studio.github.io/initiative/running-a-server/native-packages/", + "website": "https://beyonders-studio.github.io/initiative/", + "repository": "https://github.com/beyonders-studio/initiative", + "architectures": ["amd64", "arm64"], + "platforms": ["pve"], + "logo": "https://raw.githubusercontent.com/beyonders-studio/initiative/main/frontend/public/icons/logo.svg", + "description": "Initiative is a shared workspace for communities, with tasks, projects, documents, calendars and dashboards. Live collaboration and configurable permissions let a household or team organize its work in one place.", + "install_methods": [ + { + "type": "default", + "script": "ct/initiative.sh", + "config_path": "/etc/initiative/server.env", + "resources": { + "cpu": 2, + "ram": 2048, + "hdd": 8, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Open https://CONTAINER-IP and accept the self-signed certificate. Create your first account immediately: it becomes the owner; subsequent public registration is disabled.", + "type": "warning" + }, + { + "text": "After an IP or domain change, update APP_URL in /etc/initiative/server.env and restart initiative-server. Use a trusted HTTPS reverse proxy for a public domain.", + "type": "info" + } + ] +} From 8c8ebf9b40a2dfc9f9f396d49b438845cda947d8 Mon Sep 17 00:00:00 2001 From: Marc Went Date: Fri, 30 Oct 2026 19:26:52 +0100 Subject: [PATCH 2/2] fix: set Jumpserver scripts base to DevScripts Set the missing default before loading the shared engine so remote invocation fetches the installer from DevScripts. Preserve explicit fork overrides and the repository-wide gate. Refresh the Initiative repair handoff and disclose the remaining adoption requirement and runtime acceptance gaps. Signed-off-by: Marc Went --- ct/jumpserver.sh | 1 + docs/guides/initiative.md | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/ct/jumpserver.sh b/ct/jumpserver.sh index c5ea85a204..f2c5183aed 100644 --- a/ct/jumpserver.sh +++ b/ct/jumpserver.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main" _cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func" source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func") diff --git a/docs/guides/initiative.md b/docs/guides/initiative.md index 7173706a0e..fa269c3220 100644 --- a/docs/guides/initiative.md +++ b/docs/guides/initiative.md @@ -3,7 +3,7 @@ This test installer is proposed for [DevScripts](https://github.com/community-scripts/DevScripts), the required testing stage before a new script can reach ProxmoxVE. It is not an accepted -Community Script. Initiative has 191 stars as of 2026-10-30, below DevScripts' +Community Script. Initiative has 192 stars as of 2026-10-30, below DevScripts' 600-star requirement and the ProxmoxVE script-request form's 1,000-star requirement. Consideration requires a maintainer exception; neither threshold is claimed to be met. @@ -228,6 +228,40 @@ checks confirmed that version and healthy readiness. The corrected full updater has not been rerun on that container. Browser interactions, reboot persistence, and preservation of real user data across the upgrade remain unverified. +## PR #2484 CI repair handoff + +The `ct scripts base` workflow checks every CT script, including existing ones. +Its failure on the initial Initiative submission came from `ct/jumpserver.sh`, +the only script missing `_CS_DEFAULT_URL`. The repair sets that value before +loading the engine, so a remotely fetched Jumpserver script selects DevScripts' +installer instead of falling back to ProxmoxVE. Initiative already declares the +correct default. An explicit `COMMUNITY_SCRIPTS_URL` still selects a fork or +branch for either script; keep it exported when testing this PR. + +The repair preserves the native installer, updater, checksum verification, +database backups and all 35 offline regression cases. Applicable local checks +are the commands above, Bash syntax and ShellCheck for `ct/jumpserver.sh`, +`actionlint .github/workflows/ct-base.yml .github/workflows/bash-syntax.yml`, +and the complete scripts-base and syntax loops from those workflows. Local +source-resolution checks exercise the shared engine's default and explicit URL +selection without running an installer. These checks establish source routing +and shell behavior, not a container deployment. + +Jumpserver's existing updater has seven ShellCheck `SC2164` warnings about +unguarded directory changes. The repair adds no diagnostics; these pre-existing +warnings remain outside this scripts-base repair. Initiative's scripts and +regression harness pass ShellCheck with the framework exclusions above. + +The separate PR requirements failure is still the unmet **600+ GitHub stars** +requirement. The upstream repository currently has 192 stars, was created on +2025-11-22, remains active and publishes official releases and native packages. +Keep the star checkbox unchecked and the PR in draft. Maintainers must decide +the adoption exception; do not relabel the contribution or bypass the check. +Fresh corrected-branch Proxmox testing, browser/file/live-editor behavior, reboot +persistence and the corrected full updater's preservation of real user data +remain operator acceptance work in issue #2483. No deployment is part of this +CI repair. + ## Sources - [Initiative v0.76.0 release and server assets](https://github.com/beyonders-studio/initiative/releases/tag/v0.76.0)