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/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 new file mode 100644 index 0000000000..fa269c3220 --- /dev/null +++ b/docs/guides/initiative.md @@ -0,0 +1,274 @@ +# 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 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. + +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. + +## 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) +- [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" + } + ] +}